Данное руководство содержит справочную информацию по платформе CUBA и охватывает наиболее важные темы разработки бизнес-приложений на платформе.
Для успешной работы с платформой требуется знание следующих технологий:
Для глубокого понимания принципов работы платформы полезным является знакомство со следующими технологиями и фреймворками:
Настоящее руководство, а также другая документация по платформе CUBA, доступны по адресу www.cuba-platform.ru/manual. Обучающие видео-материалы и презентации располагаются по адресу www.cuba-platform.ru/tutorials. Онлайн демо-приложения могут быть запущены со страницы www.cuba-platform.ru/online-demo.
Если у Вас имеются предложения по улучшению данного руководства, мы будем рады принять ваши pull request’ы и issues в исходниках документации на GitHub. Если вы увидели ошибку или несоответствие в документе - пожалуйста, форкните репозиторий и исправьте проблему. Заранее спасибо!
В данной главе приводятся сведения о назначении и возможностях платформы CUBA.
Платформа CUBA предназначена для разработки корпоративных решений, характеризующихся сложной моделью данных, десятками или сотнями экранов, большим количеством бизнес-логики, а также требованиями к контролю прав доступа, масштабируемости и отказоустойчивости.
Широкий набор готовой функциональности, развитые средства генерации кода, визуальный дизайнер интерфейсов, а также поддержка hot deploy радикально сокращают время и стоимость разработки решения.
Платформа полностью построена на стеке открытых Java технологий, что позволяет использовать наработки крупнейшей экосистемы свободного ПО в мире, а также контролировать исходный код всего стека. Открытая архитектура позволяет переопределять поведение большинства механизмов платформы, обеспечивая высокую гибкость. Разработчики могут использовать популярные Java IDE и имеют полный доступ к исходному коду.
Приложения на платформе легко встраиваются в ИТ-инфраструктуру благодаря поддержке основных баз данных и серверов приложений, а также возможности работы в облаке. Платформа позволяет обеспечить отказоустойчивость и масштабируемость решений, а универсальный REST API предоставляет средства интеграции с внешними системами.
Минимальные требования для ведения разработки на платформе CUBA:
Список изменений в платформе доступен по адресу http://files.cuba-platform.com/cuba/release-notes/6.10
2. Установка и настройка инструментария
Минимально необходимым набором программного обеспечения является:
Java SE Development Kit (JDK) 8
java -version
В ответ должно быть выведено сообщение с номером версии Java, например 1.8.0_152.
|
Java 9 пока не поддерживается. Собирать и запускать CUBA-приложения можно только на Java 8. |
Для сборки и запуска проектов вне Studio в переменной окружения JAVA_HOME необходимо установить путь к корневому каталогу JDK, например C:\Program Files\Java\jdk1.8.0_152.
export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
Если вы установили Java 9 на macOS, CUBA Studio и приложения перестанут запускаться, независимо от значения JAVA_HOME. В этом случае обратитесь к информации в разделе решение проблем ниже.
Cреда разработки на Java
IntelliJ IDEA или Eclipse. Рекомендуется использовать IntelliJ IDEA (Community или Ultimate).
База данных
В простейшем случае в качестве сервера баз данных приложений используется встроенный HyperSQL (http://hsqldb.org), что вполне подходит для исследования возможностей платформы и прототипирования приложений. Для создания реальных приложений рекомендуется установить и использовать в проекте какую-либо из полноценных СУБД, поддерживаемых платформой, например PostgreSQL.
Веб-браузер
Веб-интерфейс приложений, создаваемых на основе платформы, поддерживает все популярные современные браузеры, в том числе Google Chrome, Mozilla Firefox, Safari, Opera 15+, Internet Explorer 9+, Microsoft Edge.
Окружение
java -version
В ответ должно быть выведено сообщение с номером версии Java, например 1.8.0_152.
sudo apt-get install openjfx
Рекомендуется установить нужные свойства в переменной окружения JAVA_OPTS. Скрипт запуска Studio передает JAVA_OPTS в java.exe.
Новая установка Studio
Когда запустится веб-сервер, в поле URL отобразится адрес, по которому доступен интерфейс Studio. Нажав →, можно открыть веб-браузер, нажав Copy − скопировать адрес в буфер обмена.
Если по какой-либо причине Вы хотите использовать уже установленный на компьютере Gradle, введите в поле путь к соответствующему каталогу. Текущая версия системы сборки проектов протестирована на Gradle 4.3.1.
Обновление CUBA Studio
Если вы обновляете Studio на новую bug-fix версию (например с 6.5.0 на 6.5.1), устанавливайте ее в существующий каталог, например на Windows это будет C:\Program Files (x86)\CUBA Studio 6.5. При установке новой minor или major версии, используйте отдельный каталог, например CUBA Studio 6.6.
Если Studio установлена из инсталлятора Windows EXE или из ZIP-архива, она поддерживает автообновление на новые bug-fix релизы. Файлы обновлений сохраняются в каталоге ~/.haulmont/studio/update. В случае каких-либо проблем с новой версией вы можете просто удалить файлы обновления и Studio вернется к версии, установленной ранее вручную.
Автообновление не работает для minor и major релизов, и если Studio была установлена из macOS DMG. В этом случае загрузите и запустите новый инсталлятор вручную.
2.2. Интеграция CUBA Studio с IDE
Для интеграции с IntelliJ IDEA или Eclipse выполните следующие шаги:
Обратите внимание, что в панели статуса Studio загорелась надпись IDE: on port 48561. Теперь при нажатии кнопок IDE в Studio соответствующие файлы исходных кодов будут открываться редактором IDE.
|
Если вы используете CUBA CLI, перейдите в каталог проекта в используемом терминале и выполните команду gradlew idea, чтобы создать необходимые файлы для интеграции с IntelliJ IDEA. |
В данном разделе рассматривается создание приложения при помощи CUBA Studio. Эта же информация изложена в видеороликах, доступных по адресу www.cuba-platform.ru/quickstart.
На Вашей рабочей машине уже должно быть установлено и настроено необходимое программное обеспечение, см. Установка и настройка инструментария.
Основные задачи, стоящие при разработке нашего приложения:
Приложение предназначено для ведения сведений о покупателях и их заказах.
Покупатель имеет следующие характеристики:
Характеристики заказа:
Пользовательский интерфейс приложения должен содержать:
Логин и пароль пользователя − admin / admin.
Запущенное приложение содержит два главных пункта меню (Administration и Help), функциональность подсистемы безопасности и администрирования системы.
Создадим класс сущности Customer (Покупатель).
Далее создадим атрибуты сущности. Для этого нажмите на кнопку New, находящуюся под таблицей Attributes.
Для добавления атрибута нажмите на кнопку Add.
После создания атрибутов перейдите на вкладку Instance Name дизайнера сущности для задания Name pattern. В списке Available attributes выделите атрибут name и перенесите его в список Name pattern attributes, нажав на кнопку с изображением стрелки вправо.
На этом создание сущности Customer завершено. Нажмите на кнопку OK в верхней панели для сохранения изменений.
Создадим сущность Order (Заказ). В панели DATA MODEL нажмите на кнопку New > Entity. В поле Class name введите название класса сущности − Order. Сущность должна иметь следующие атрибуты:
3.4. Создание таблиц базы данных
Для создания таблиц базы данных достаточно на вкладке Data Model панели навигатора нажать на кнопку Generate DB scripts. После этого откроется страница Database Scripts. На вкладке будут сгенерированы скрипты обновления базы данных от ее текущего состояния (UPDATE SCRIPTS) и скрипты создания базы данных с нуля (INIT TABLES, INIT TABLES, INIT DATA). Также на вкладке будут доступны уже выполненные скрипты обновления базы данных, если они есть.
Чтобы сохранить сгенерированные скрипты, нажмите на кнопку Save and close. Для запуска скриптов обновления остановите запущенное приложение с помощью команды Run → Stop application server, затем выполните Run → Update database.
3.5. Создание экранов пользовательского интерфейса
Создадим экраны приложения, позволяющие управлять информацией о покупателях и заказах.
3.5.1. Экраны управления Покупателями
Для создания стандартных экранов просмотра и редактирования покупателей необходимо выделить сущность Customer на вкладке Data Model панели навигатора и нажать на кнопку New > Generic UI screen внизу панели. После этого на экране отобразится страница создания стандартных экранов сущности.
В списке доступных шаблонов выберите Entity browser and editor screens.
Все поля этого окна заполнены значениями по умолчанию, пока не будем их менять. Нажмите на кнопку Create и затем Close.
Во вкладке Generic UI панели навигатора в модуле Web Module появятся элементы customer-browse.xml и customer-edit.xml.
3.5.2. Экраны управления Заказами
Сущность Order (Заказ) имеет следующую особенность: так как среди прочих атрибутов существует ссылочный атрибут Order.customer, требуется определить представление сущности Order, включающее этот атрибут (стандартное представление _local не включает ссылочных атрибутов).
Для этого перейдите на вкладку Data Model на панели навигатора, выделите сущность Order и выберите New > View. Отобразится страница дизайнера представлений. В качестве имени введите order-with-customer, в списке атрибутов нажмите на атрибут customer и на отобразившейся справа панели выберите представление _minimal для сущности Customer.
Нажмите на кнопку OK в верхней панели.
Далее выделите сущность Order и выберите New > Generic UI screen.
В отобразившемся окне выберите представление order-with-customer в полях View для браузера и редактора и нажмите на кнопку Create и, затем, Close.
Во вкладке Generic UI панели навигатора в модуле Web Module появятся элементы order-edit.xml и order-browse.xml.
При создании экраны были добавлены в пункт меню application, имеющийся по умолчанию. Переименуем его. Для этого перейдите на вкладку Generic UI на панели навигатора и нажмите на ссылку Open web menu. Отобразится страница дизайнера меню. Выделите пункт меню application-sales для просмотра его свойств.
В поле Id введите новое значение идентификатора меню − shop. После редактирования меню нажмите на кнопку OK в верхней панели.
3.5.4. Экран редактирования Покупателя со списком Заказов
Займемся задачей отображения списка заказов в окне редактирования покупателя.
select e from sales$Order e where e.customer.id = :ds$customerDs order by e.date
Здесь запрос содержит условие отбора Заказов с параметром ds$customerDs. Значением параметра с именем вида ds${datasource_name} будет идентификатор сущности, установленной в данный момент в источнике данных datasource_name, в данном случае − идентификатор редактируемого Покупателя.
|
Если разрабатываемое приложение предполагает локализацию на несколько языков, используйте кнопку |
сгенерируйте идентификатор таблицы: ordersTable.
Посмотрим, как созданные нами экраны выглядят в работающем приложении. Для этого выполните Run > Start application server.
Зайдите в систему, использовав стандартные имя и пароль в окне логина. Откройте пункт меню Shop > Customers:
Рисунок 1. Экран списка Customers
Нажмите на кнопку Create и создайте нового покупателя:
Рисунок 2. Экран редактирования Customer
Откройте пункт меню Shop > Orders:
Рисунок 3. Экран списка Orders
Нажмите на кнопку Create и создайте новый заказ, выбрав в поле Customer только что созданного покупателя:
Рисунок 4. Экран редактирования Order
В таблице на экране редактирования покупателя теперь отображается только что созданный заказ:
Рисунок 5. Экран редактирования Customer
Данный раздел представляет собой коллекцию практических рецептов по решению типичных задач, встающих перед разработчиками при использовании платформы CUBA. В каждом подразделе информация организована от простого к сложному, поэтому вы в любой момент можете перейти к другой секции или заняться кодированием.
Большинство разделов снабжены демо-приложениями. Вы можете увидеть их в работе онлайн, посмотреть исходный код на GitHub, или загрузить и запустить локально. Ссылки на проекты приложений доступны также на вкладке Samples в Studio.
|
Данный раздел находится в работе и будет постоянно дополняться. Если у вас есть идеи новых рецептов и задач, решение которых стоило бы продемонстрировать, вы можете создать запрос в виде issue в исходниках документации на GitHub. |
4.1. Организация бизнес-логики
Один из первых вопросов, возникающих в начале процесса разработки на платформе, это "где мне расположить мою бизнес-логику"? Использование Studio сильно облегчает создание модели данных и CRUD экранов, любой реальный проект требует создания логики помимо загрузки и сохранения данных. Данный раздел содержит информацию о том, как эффективно организовать бизнес-логику в зависимости от задачи.
Большинство примеров в данном разделе работают со следующей моделью:
В этих примерах мы будем рассчитывать скидки для заказчиков в зависимости от общей суммы их покупок.
4.1.1. Бизнес-логика в контроллерах
Например, необходимо запускать расчет скидки, когда пользователь нажимает кнопку на экране-браузере заказчиков. В этом случае, наиболее простое решение - это разместить логику расчета прямо в контроллере экрана.
См. кнопку Calculate discount в демо-приложении и реализацию контроллера: CustomerBrowse.java. Пожалуйста, имейте в виду, что данная имплементацию расчета не является оптимальной (см. варианты работы с данными в разделе Загрузка и сохранение данных).
Данный подход приемлем, если логика вызывается из одного места и она не слишком сложна, чтобы уместиться в нескольких коротких методах.
4.1.2. Использование бинов клиентского уровня
Теперь давайте немного усложним задачу из предыдущего раздела. Допустим, требуется вызывать расчет из двух экранов: и из браузера, и из редактора. Чтобы не дублировать код, нужно извлечь код из контроллера и поместить в некоторое общедоступное место. Это может быть управляемый бин клиентского уровня.
Управляемый бин - это класс с аннотацией @Component. Он может быть инжектирован в другие бины и контроллеры экранов, или получен с помощью статического метода AppBeans.get(). Если класс бина реализует некоторый интерфейс, то к нему можно обращаться через этот интерфейс.
Имейте в виду, что для того чтобы бин был доступен для контроллеров экранов, он должен располагаться в одном из следующих модулей: global, gui или web вашего проекта. В случае global, бин будет также доступен на среднем слое.
См. кнопку Calculate discount на экранах браузера и редактора в демо-приложении, и реализацию:
4.1.3. Использование сервисов среднего слоя
В предыдущем разделе мы рассмотрели инкапсуляцию бизнес-логики в бине клиентского уровня. Теперь мы пойдем дальше и поместим нашу логику в наиболее подходящее место: на средний слой. Сделав это, мы достигнем следующих целей:
Чтобы вызвать бизнес-метод среднего слоя, необходимо создать сервис. Studio может помочь в создании заготовки сервиса:
См. пример реализации в демо-приложении:
Обратите внимание, что данный бин отличается от рассмотренного в предыдущем разделе: он расположен в модуле core и использует EntityManager для загрузки суммы заказов из базы данных.
Теперь давайте сделаем наш бизнес-метод доступным для внешних клиентов через REST API:
Studio создаст файл rest-services.xml и зарегистрирует в нем метод. После перезапуска сервера вы сможете вызвать метод с помощью HTTP-запросов. Например, следующий GET-запрос должен работать с нашим онлайн демо-сервером:
https://demo1.cuba-platform.com/business-logic/rest/v2/services/sample_DiscountService/calculateDiscount?customerId=1797f54d-5bec-87a6-4330-d958955743a2
Имейте в виду, что демо-приложение имеет анонимный доступ. В большинстве реальных сценариев использования необходимо будет аутентифицироваться, прежде чем выполнять запросы.
4.1.4. Использование Entity Listeners
Entity listeners позволяют выполнять бизнес-логику каждый раз, когда сущность создается, изменяется или удаляется в базе данных. Например, мы можем пересчитывать скидку для заказчика каждый раз, когда некоторый заказ для него изменяется.
Заготовку для entity listener можно легко создать в Studio:
См. пример в демо-приложении:
Если вы откроете экран Logic in Entity Listeners демо-приложения, вы увидите две таблицы: заказы и заказчики. Создайте, измените или удалите заказ, обновите таблицу заказчиков, и вы увидите, что скидка для соответствующего заказчика изменилась.
4.1.5. Использование JMX-бинов
С помощью JMX-бинов можно предоставить доступ к некоторой административной функциональности вашего приложения без создания пользовательского интерфейса для нее. Данная функциональность будет также доступна через встроенную JMX-консоль и через внешние инструменты JMX, например jconsole.
В нашем примере со скидками пользователь, имеющий доступ к JMX-консоли, сможет пересчитывать скидки для всех заказчиков или для заказчика с указанным id.
Studio на данный момент не умеет создавать заготовки JMX-бинов, поэтому все классы и конфигурационные элементы придется создавать вручную в IDE.
См. пример реализации в демо-приложении:
4.1.6. Запуск кода на старте приложения
Иногда бывает необходимо выполнить некоторый код сразу после старта приложения в момент, когда все механизмы гарантированно работоспособны. Для этого можно воспользоваться слушателями событий жизненного цикла.
В данном разделе мы рассмотрим, как динамически зарегистрировать для сущности entity listener на старте приложения. Возьмем следующую задачу: в проекте имеется сущность Employee (сотрудник компании), которая связана один-к-одному с платформенной сущностью User (пользователь системы):
Если атрибут name сущности User изменяется, например через стандартный экран управления пользователями, необходимо, чтобы изменялся также и атрибут name связанной сущности Employee. Это обычная задача для "денормализованных" данных, и решается она, как правило, с использованием entity listeners. В данном случае ситуация осложняется тем, что необходимо отслеживать изменения не проектной, а платформенной сущности User, и добавить entity listener с помощью аннотации @Listeners невозможно. Однако, можно добавить listener динамически через бин EntityListenerManager, и сделать это лучше всего на старте приложения.
В результате сразу после старта блока Middleware будет вызван метод initEntityListeners() класса AppLifecycle. В этом методе в качестве entity listener сущности User регистрируется бин sample_UserEntityListener.
Метод onBeforeUpdate() класса UserEntityListener будет вызываться перед каждым сохранением изменений экземпляров User в базу данных. В методе проверяется, есть ли атрибут name среди измененных, и если да, загружается связанный экземпляр Employee, и в нем устанавливается это же значение name.
4.2. Моделирование предметной области
В данном разделе приведены рецепты, связанные с дизайном модели данных и работой с атрибутами сущностей.
4.2.1. Присвоение начальных значений
Присвоение начальных значений атрибутам новых экземпляров сущностей можно производить несколькими способами.
4.2.1.1. Инициализация полей сущности
Атрибуты простых типов (Boolean, Integer и т.д.) и перечисления можно инициализировать прямо в объявлении соответствующего поля класса сущности. См. например поля active и grade в Customer.java.
Кроме того, в классе сущности можно создать специальный метод инициализации и добавить ему аннотацию @PostConstruct. В этом случае в процессе инициализации можно использовать вызов любых глобальных интерфейсов инфраструктуры и бинов. См. метод init() в Customer.java.
4.2.1.2. Инициализация с помощью CreateAction
Если начальное значение атрибута зависит от данных вызывающего экрана, то можно воспользоваться методами setInitialValues() или setInitialValuesSupplier() класса CreateAction.
См. пример работы с сущностями Customer и CustomerAddress в демо-приложении:
4.2.1.3. Использование метода initNewItem
Начальные значения можно также задать в контроллере экрана создаваемой сущности в методе initNewItem().
Рассмотрим сущности:
В демо-приложении атрибут info сущности CustomerDetails редактируется в том же экране, что и сам Customer. Данный подход требует создания экземпляра CustomerDetails вместе с владеющим им Customer.
4.2.2. Редактирование композитных сущностей
Платформа CUBA поддерживает два типа связи между сущностями: ассоциацию и композицию. В интерфейсе CUBA Studio они названы соответственно ASSOCIATION и COMPOSITION. Ассоциация - это связь между объектами, которые могут существовать отдельно друг от друга. Композиция же используется для связи типа "master-detail" когда экземпляры detail существуют только в составе master. Примером композиции может служить связь аэропорта и терминалов: терминал, не относящийся ни к какому аэропорту, не имеет смысла.
Как правило, редактирование сущностей, входящих в состав композиции, удобно осуществлять совместно. То есть, например, пользователь открывает экран редактирования аэропорта, видит в нем список терминалов, может создавать и редактировать их, но все изменения, как аэропорта, так и терминалов, сохраняются в базу данных вместе в одной транзакции, и только тогда, когда пользователь подтвердит сохранение главной сущности - аэропорта.
4.2.2.1. One-to-Many: один уровень вложенности
Рассмотрим реализацию композиции на примере сущностей Airport и Terminal:
В редакторе сущностей Studio установите следующие свойства для атрибута airport: Attribute type - ASSOCIATION, Cardinality - MANY_TO_ONE, Mandatory - on.
В редакторе сущностей Studio установите следующие свойства для атрибута terminals: Attribute type - COMPOSITION, Cardinality - ONE_TO_MANY, On delete - CASCADE.
В результате редактирование экземпляра аэропорта работает следующим образом:
4.2.2.2. One-to-Many: два уровня вложенности
Композиция может быть более глубокой и состоять из двух уровней вложенности. Усложним приведенный выше пример, добавив сущность MeetingPoint, описывающую место встречи у терминала аэропорта:
Теперь сущность Terminal содержит атрибут meetingPoints - коллекцию экземпляров MeetingPoint. Для того, чтобы все три сущности представляли собой единую композицию и редактировались совместно, нужно в дополнение к описанному в предыдущем разделе выполнить следующее:
|
Источник данных meetingPointsDs здесь не связан ни с какими визуальными компонентами, однако он необходим для корректной работы совместного редактирования композиции. |
В результате измененные экземпляры MeetingPoint, так же как и экземпляры Terminal, будут сохраняться в базу данных только вместе с экземпляром Airport в одной транзакции.
4.2.2.3. One-to-Many: три уровня вложенности
Представьте, что вам необходима еще одна сущность, содержащая некоторые детали места встречи (MeetingPoint). Назовем эту сущность Note. Таким образом, вся структура будет выглядеть следующим образом: Airport > Terminal > Meeting Point > Note.
CUBA может обеспечить работу с композициями с максимум двумя уровнями вложенности. Теперь у нас структура с тремя уровнями, поэтому необходимо ограничить глубину композиции либо сверху, либо снизу. В данном разделе мы рассмотрим два различных (с точки зрения user experience) подхода для исключения из композиции аэропорта. Оба подхода решают одну и ту же проблему: так как теперь терминалы сохраняются в базу данных независимо от аэропорта, невозможно сохранить терминал для только что созданного аэропорта, пока он не сохранен в БД.
4.2.2.4. Композиция One-to-One
Композиция one-to-one рассматривается на примере сущностей Customer и CustomerDetails:
В результате редактирование экземпляра Customer работает следующим образом:
4.2.3. Ассоциация Many-to-Many
В ассоциации many-to-many множество записей в одной таблице может быть связано со множеством записей в другой таблице, при этом первичные ключи обеих связанных сущностей хранятся в отдельной связующей таблице. При необходимости связующая таблица может содержать дополнительные поля.
В зависимости от того, нужны ли вам дополнительные поля в связующей таблице, отношения many-to-many можно реализовать с использованием связующей сущности или без неё. Далее мы рассмотрим оба подхода.
4.2.3.1. Прямая ассоциация Many-to-Many
Рассмотрим реализацию ассоциации many-to-many на примере сущностей Airport и Airline. Один аэропорт может принимать множество авиакомпаний, и одна авиакомпания-перевозчик, в свою очередь, может осуществлять рейсы во множество аэропортов.
В редакторе сущностей Studio установите следующие свойства для атрибута airlines: Attribute type - ASSOCIATION, Cardinality - MANY_TO_MANY.
Сущность Airport будет отмечена ведущей стороной ассоциации, и Studio предложит создать соответствующий атрибут airports в сущности Airline на противоположной стороне отношений.
@JoinTable(name = "SAMPLE_AIRLINE_AIRPORT_LINK",
joinColumns = @JoinColumn(name = "AIRPORT_ID"),
inverseJoinColumns = @JoinColumn(name = "AIRLINE_ID"))
@ManyToMany
protected List<Airline> airlines;
@JoinTable(name = "SAMPLE_AIRLINE_AIRPORT_LINK",
joinColumns = @JoinColumn(name = "AIRLINE_ID"),
inverseJoinColumns = @JoinColumn(name = "AIRPORT_ID"))
@ManyToMany
protected List<Airport> airports;
Сущность Airline также будет отмечена как ведущая сторона отношений, что позволяет редактировать коллекции с обеих сторон.
Таким образом, экраны редактирования сущностей Airport и Airline полностью симметричны.
В результате редактирование экземпляра авиакомпании работает следующим образом:
В экране редактирования авиакомпании отображается список аэропортов.
Пользователь может нажать Add и в открывшемся экране выбора сущности Airport либо выбрать аэропорт, который нужно добавить, либо открыть экран его редактирования. При нажатии OK в экране редактирования аэропорта изменённый экземпляр аэропорта сохраняется как в базу данных, так и в источник данных airportsDs экрана редактирования авиакомпании, так как сущность Airport является полностью независимой.
Пользователь может создавать новые аэропорты или удалять их, и все изменения будут сохраняться и в базу данных в отдельных транзакциях, и в источник данных airportsDs.
Пользователь нажимает OK в экране редактирования авиакомпании, и изменённый экземпляр Airline вместе со всеми ссылками на экземпляры Airport отправляется на middleware в метод DataManager.commit(), чтобы быть сохранённым в базе данных.
На другой стороне отношений в экране редактирования сущности Aiport работает ровно тот же принцип.
4.2.3.2. Ассоциация Many-to-Many через связующую сущность
Отношения many-to-many всегда требуют создания связующей таблицы в базе данных, однако создание отдельной сущности для отражения этой таблицы является опциональным. Связующую сущность необходимо создать в том случае, если вы хотите хранить в связующей таблице некие дополнительные поля.
Продемонстрируем этот подход на примере сущностей Airport и DutyFree. В одном аэропорту может располагаться множество разных сетей магазинов беспошлинной торговли, и одна сеть duty-free может быть представлена во множестве разных аэропортов. Предположим, что кроме связи сущностей мы хотим хранить ещё и валюту, используемую в данном магазине в данном аэропорту:
В редакторе сущностей Studio установите следующие свойства для атрибута dutyFreeShops: Attribute type - COMPOSITION, Cardinality - ONE_TO_MANY.
@Composition
@OnDelete(DeletePolicy.CASCADE)
@OneToMany(mappedBy = "airport")
protected List<AirportDutyFree> dutyFreeShops;
В редакторе сущностей Studio установите следующие свойства для атрибута airports: Attribute type - COMPOSITION, Cardinality - ONE_TO_MANY.
@Composition
@OnDelete(DeletePolicy.CASCADE)
@OneToMany(mappedBy = "dutyFree")
protected List<AirportDutyFree> airports;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "AIRPORT_ID")
protected Airport airport;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DUTY_FREE_ID")
protected DutyFree dutyFree;
Представление dutyFree-airport следует той же логике: оно содержит атрибут-коллекцию airports (со ссылками на AirportDutyFree), а в ней атрибуты airport и currency.
В результате редактирование экземпляра DutyFree работает следующим образом:
В экране редактирования DutyFree отображается таблица аэропортов и выпадающий список валют.
По нажатию Add airport открывается экран выбора Airport, и пользователь может как выбрать аэропорт, так и открыть экран его редактирования. Когда экземпляр аэропорта выбран, создаётся новый экземпляр сущности AirportDutyFree, которому проставляется валюта по умолчанию. Этот экземпляр связующей сущности не сохраняется в базу данных, а добавляется в источник данных airportsDs экрана редактирования DutyFree.
Когда пользователь нажимает OK в экране редактирования аэропорта, изменённый экземпляр аэропорта сохраняется и в базу данных, и в источник данных airportsDs экрана редактирования DutyFree, так как сущность Airport является полностью независимой.
Пользователь может сохранять новые аэропорты или удалять их, и все изменения будут сохраняться в базу данных в отдельных транзакциях, а также в источник данных airportsDs.
Когда пользователь нажимает OK в экране редактирования магазина, изменённый экземпляр DutyFree вместе со всеми измененными экземплярами AirportDutyFree отправляется на middleware в метод DataManager.commit() и сохраняется в базе данных в рамках одной транзакции.
Рассмотрим пример использования наследования сущностей в приложении CUBA.
Рассмотрим типичный случай, когда у заказа могут быть заказчики разных типов - например, юридические и физические лица, или сущности Company и Individual, у которых есть общие атрибуты. Мы хотим хранить общие атрибуты в общей таблице, а специфичные атрибуты для каждого типа - в отдельных связанных таблицах.
Так, в модели данных мы создадим сущность Client как базовый класс, который будем хранить в таблице SAMPLE_CLIENT. Сущности Company и Person будут храниться в отдельных таблицах с внешним ключом, ссылающимся на базовую таблицу.
Сущность Order (заказ) имеет ссылку на сущность Client. Поскольку заказчики могут быть нескольких типов, при создании нового заказа пользователь должен иметь возможность выбрать нужный тип.
Сущность Client.java:
Сущность Company.java:
Сущность: Person.java:
Контроллер экрана OrderEdit.java содержит визуальные компоненты и логику для выбора типа заказчика.
Данный раздел затрагивает темы, связанные с подсистемой Generic UI, являющейся основной технологией создания frontend в CUBA-приложениях.
4.3.1. Правила компоновки экранов
Ниже объясняется, как правильно располагать визуальные компоненты и контейнеры на экранах UI.
4.3.1.1. Позиционирование компонентов
Размеры компонента, т.е. его ширина и высота, могут быть заданы следующих видов:
Компонент займет столько места, сколько нужно его содержимому.
Примеры:
XML
<label width=”AUTO”/>
Java
label.setWidth(Component.AUTO_SIZE);
Компоненты с размером по содержимому будут подстраивать размер во время компоновки экрана и при изменении размера содержимого.
Фиксированные размеры не предполагают изменения размера компонента во время исполнения.
XML
<vbox width=”320px” height=”240px”/>
Java
vbox.setWidth(”320px”);
Относительные размеры указывают, какой процент доступного компоненту места будет использован.
XML
<label width=”100%”/>
Java
label.setWidth(”50%”);
Компонент с относительными размерами будет реагировать на изменение доступного места и изменять свой реальный размер на экране.
По умолчанию контейнеры без установленного атрибута expand выделяют для всех вложенных компонентов одинаковое количество места. Исключения: flowBox и htmlBox.
Пример контейнера с одинаковой высотой компонентов по умолчанию:
<layout>
<button caption="Button"/>
<button caption="Button"/>
</layout>
Компоненты и контейнеры при создании имеют высоту и ширину по содержимому. Некоторые контейнеры имеют другие значения высоты и ширины по умолчанию:
|
Контейнер |
Ширина |
Высота |
|
100% |
AUTO | |
|
100% |
AUTO | |
|
100% |
AUTO |
Корневой элемент компоновки layout является вертикальным контейнером (VBox) и имеет 100% ширину и высоту 100%. В режиме диалога высота корневого элемента может быть AUTO.
Вкладка компонента TabSheet (tab) является контейнером VBox.
Компонент GroupBox содержит VBox или HBox в зависимости от значения свойства orientation.
Пример контейнера с высотой по содержимому:
<layout>
<vbox>
<button caption="Button"/>
<button caption="Button"/>
</vbox>
</layout>
Пример контейнера с относительными размерами компонентов:
<layout spacing="true">
<groupBox caption="GroupBox" height="100%">
</groupBox>
<button caption="Button"/>
</layout>
Здесь layout, так же как vbox или hbox, выделяет равные части всем вложенным компонентам, а для groupBox указана высота 100%. Кроме того, groupBox имеет 100% ширину по умолчанию, поэтому он занимает все доступное ему пространство.
Для Table и Tree рекомендуется задавать абсолютную или относительную высоту, иначе таблица/дерево может неограниченно вырасти при большом количестве строк/узлов.
Контейнер ScrollBox должен обязательно иметь заданные высоту и ширину (не AUTO). Внутри ScrollBox нельзя использовать 100% размеры в направлении, для которого необходима полоса прокрутки.
Ниже приведены примеры правильного использования ScrollBox с вертикальной и горизонтальной прокруткой. Если требуются обе полосы прокрутки, компоненты должны иметь и ширину, и высоту (AUTO или абсолютные значения).
Атрибут expand контейнера позволяет указать, какому из компонентов предоставить максимальное доступное место.
Компоненту, указанному в expand, будет выставлен размер 100% в направлении роста контейнера (VBox — по вертикали, HBox — по горизонтали). При изменении размера контейнера изменять размер будет именно этот компонент.
<vbox expand=”bigBox”>
<vbox id=”bigBox”>
</vbox>
<label value=”Label”/>
</vbox>
expand работает по направлению роста контейнера, например:
<layout spacing="true" expand="groupBox">
<groupBox id="groupBox"
caption="GroupBox" width="200px">
</groupBox>
<button caption="Button"/>
</layout>
В следующем примере используется вспомогательный элемент Label - spacer. Для него применяется expand, поэтому он занимает всё оставшееся в контейнере место.
<layout expand="spacer">
<textField caption="Number"/>
<dateField caption="Date"/>
<label id="spacer"/>
<hbox spacing="true">
<button caption="OK"/>
<button caption="Cancel"/>
</hbox>
</layout>
Отступ от границ контейнера (margin)
Атрибут margin позволяет задать отступ вложенных компонентов от края контейнера.
Если задан margin="true", то отступ применяется для всех сторон контейнера.
<layout>
<vbox margin="true" height="100%">
<groupBox caption="Group"
height="100%">
</groupBox>
</vbox>
<groupBox caption="Group"
height="100%">
</groupBox>
</layout>
Можно также задать отступ для каждой из сторон отдельно (в порядке Верхний, Правый, Нижний, Левый). Пример использования только верхнего и нижнего отступа:
<vbox margin="true,false,true,false">
Отступ между компонентами контейнера (spacing)
Атрибут spacing указывает, использовать ли отступ между вложенными компонентами по направлению роста контейнера.
|
Не используйте margin для эмуляции spacing. Spacing работает правильно в случаях, когда часть компонентов контейнера становится невидимой. |
<layout spacing="true">
<button caption="Button"/>
<button caption="Button"/>
<button caption="Button"/>
<button caption="Button"/>
</layout>
Выравнивание компонентов в контейнере
Для выравнивания компонентов в контейнере воспользуйтесь атрибутом align.
Пример расположения надписи по центру контейнера:
<vbox height="100%">
<label align="MIDDLE_CENTER"
value="Label"/>
</vbox>
Компонент, для которого задан align, не должен иметь размер "100%" в направлении выравнивания. В контейнере должно быть доступное для компонента место, по размеру большее чем сам компонент. Именно в этом пространстве будет выровнен компонент.
Пример выравнивания в доступном пространстве:
<layout>
<groupBox height="100%"
caption="Group">
</groupBox>
<label align="MIDDLE_CENTER"
value="Label"/>
</layout>
4.3.1.4. Типовые ошибки компоновки
Ошибка №1. Указание относительных размеров для компонента в контейнере с размерами по содержимому
Пример неправильной компоновки c явным относительным размером:
В этом примере для надписи задана высота 100%. При этом у контейнера VBox по умолчанию используется высота AUTO, то есть по содержимому.
Пример неправильной компоновки c expand:
Expand неявно задаёт относительную высоту 100% для label, что, как и в примере выше, неверно. В таких случаях экран может выглядеть некорректно, часть компонентов может пропадать или иметь нулевые размеры. При возникновении проблем с компоновкой в первую очередь проверьте правильность указания относительных размеров.
Ошибка №2. Вложенные в ScrollBox компоненты имеют 100% размеры
Пример неправильной компоновки:
При возникновении таких ошибок полосы прокрутки в ScrollBox не будут появляться при превышении вложенными компонентами размеров области прокрутки.
Ошибка №3. Выравнивание для компонентов при отсутствии доступного места
Пример неправильной компоновки:
В этом примере HBox имеет размеры по содержимому, поэтому заданное для надписи выравнивание не оказывает никакого эффекта.
4.3.2. Передача параметров в экран
Передача параметров из одного экрана в другой является одной из самых частых задач в разработке UI. Рассмотрим типовые решения этой задачи на примере демо-приложения "управление заказами".
При открытии экрана методом openWindow
Параметры могут быть переданы в мэп, являющейся опциональным параметром методов openWindow(), openLookup() и openEditor(). Они будут доступны в открываемом экране в виде мэп, передаваемой в метод init(), а также индивидуально, если они инжектируются с помощью аннотации @WindowParam.
Предположим, что мы хотим отфильтровать список продуктов в экране просмотра сущности Product, передав в него некоторые параметры из экрана редактирования сущности Order.
Когда пользователь выбирает продукт, открывается экран QuantityDialog, где пользователь вводит количество единиц выбранного продукта. После закрытия этого экрана создаётся новый экземпляр сущности OrderLine и затем добавляется в таблицу строк заказа.
openLookup("sample$Product.browse",
items -> {
if (!items.isEmpty()) {
openQuantityDialog((Product) items.iterator().next());
}
},
WindowManager.OpenType.THIS_TAB,
ParamsMap.of(
"customer", getItem().getCustomer(),
"added", orderLinesDs.getItems().stream()
.map(line -> line.getProduct().getId())
.collect(Collectors.toList())
)
);
@WindowParam
private Customer customer;
@Override
public void init(Map<String, Object> params) {
if (customer != null) {
productsDs.setQuery(
"select e from sample$Product e left join e.customer c " +
"where c.id = :param$customer or c is null");
}
}
Также, когда экран выбора продукта открывается для создания строк заказа, в нём программно создаётся и применяется фильтр, который отображает только те продукты, которые ещё не были использованы в этом заказе.
|
Фильтры нужно создавать в методе ready(), так как на момент вызова метода init() фильтры ещё не инициализированы. |
@WindowParam
private List<UUID> added;
@Override
public void ready() {
if (added != null && !added.isEmpty()) {
FilterEntity filterEntity = metadata.create(FilterEntity.class);
filterEntity.setName("Not added yet");
filterEntity.setXml("<filter>\n" +
" <and>\n" +
" <c name=\"id\" class=\"java.util.UUID\" inExpr=\"true\" hidden=\"true\" operatorType=\"NOT_IN\" width=\"1\" type=\"PROPERTY\">" +
" <![CDATA[((e.id not in :component$filter.id_list) or (e.id is null)) ]]>\n" +
" <param name=\"component$filter.id_list\" javaClass=\"java.util.UUID\">NULL</param>\n" +
" </c>\n" +
" </and>\n" +
"</filter>");
filter.setFilterEntity(filterEntity);
filter.setParamValue("id_list", added);
filter.apply(true);
}
}
Содержимое атрибута FilterEntity.xml может быть взято из фильтра, созданного при работе приложения: откройте Entity Inspector, найдите созданный фильтр, сохраненный как экземпляр сущности sec$Filter, и скопируйте его XML.
При открытие экранов из PickerField
Компонент PickerField и компоненты, его расширяющие, также могут передавать параметры в открываемые экраны. Параметры задаются для действий PickerField: LookupAction и OpenAction.
Допустим, мы хотим изменять заголовок экрана выбора сущности Customer, если он был открыт из компонента PickerField в экране редактирования сущности Product.
public class ProductEdit extends AbstractEditor<Product> {
@Named("fieldGroup.customer")
private PickerField customerField;
@Override
protected void postInit() {
customerField.getLookupAction().setLookupScreenParams(ParamsMap.of("product", getItem()));
}
}
@WindowParam
private Product product;
@Override
public void init(Map<String, Object> params) {
if (product != null && product.getName() != null) {
getFrame().setCaption("Select a customer for " + product.getName());
}
}
Теперь, если экран выбора покупателя открывается из редактора продукта, мы сразу можем увидеть, что это за продукт.
4.3.3. Возврат значений из экрана
Методы, используемые для открытия экранов (openWindow(), openLookup(), openEditor()), позволяют также и возвращать значения из этих экранов.
Возврат значения из экрана выбора
Метод openLookup() позволяет задать обработчик для сущностей, выбранных в открываемом экране. В нашем примере, с помощью этого обработчика, реализованного лямбда-выражением, мы установим выбранного покупателя для редактируемого экземпляра сущности Order.
openLookup("sample$Customer.browse",
items -> {
if (!items.isEmpty()) {
getItem().setCustomer((Customer) items.iterator().next());
}
},
WindowManager.OpenType.DIALOG.setWidth("600px").setHeight("400px"));
Возврат значения из произвольного экрана
Идея состоит в том, чтобы вернуть ссылку на контроллер открываемого экрана, и к этой ссылке добавить слушатель CloseListener, который будет обрабатывать значения после закрытия экрана.
Экран OrderEdit демонстрирует два способа выбора сущности Customer: из экрана выбора (lookup) и из независимого экрана, и оба способа позволяют вернуть экземпляр Customer.
Метод openWindow() в следующем примере открывает список покупателей в простом диалоговом окне. Слушатель CloseWithCommitListener получает сообщение о закрытии экрана действием с Window.COMMIT_ACTION_ID и устанавливает выбранного покупателя для редактируемого экземпляра сущности Order.
CustomerList window = (CustomerList) openWindow("customer-list", WindowManager.OpenType.DIALOG);
window.addCloseWithCommitListener(() -> {
getItem().setCustomer(window.getSelectedCustomer());
});
4.3.4. Композиция One-to-One в одном редакторе
Сущности, связанные композицией с типом один-к-одному, часто бывает удобно создавать в одном общем экране редактирования. Рассмотрим, как можно реализовать такой экран, на примере отношений сущностей Customer и CustomerDetails.
<dsContext>
<datasource id="customerDs"
class="sample.entity.Customer"
view="customer-view">
<datasource id="detailsDs"
property="details"/>
</datasource>
</dsContext>
Поля для редактирования обеих сущностей сгруппированы в один компонент fieldGroup, где отдельные поля привязаны к вложенному источнику данных:
<fieldGroup id="customerGroup"
datasource="customerDs">
<column width="200px">
<field property="name"/>
<field property="email"/>
<field datasource="detailsDs"
property="address"
rows="3"/>
<field datasource="detailsDs"
property="note"
rows="3"/>
</column>
</fieldGroup>
@Inject
private Metadata metadata;
@Override
protected void initNewItem(Customer customer) {
customer.setDetails(metadata.create(CustomerDetails.class));
}
Напоследок обработаем ситуацию, когда пользователь нажимает Create, а затем закрывает редактор экрана без каких-либо изменений в полях покупателя. По умолчанию, пользователю будет предложено сохранить или отменить изменения, так как detailsDs уже содержит пустой экземпляр CustomerDetails, а значит, метод isModified() вернёт true. Чтобы предотвратить появление такого диалога для пустых сущностей, заставим метод isModified() принимать во внимание только изменения в главном источнике данных экрана:
@Override
public boolean isModified() {
return customerDs.isModified();
}
Теперь обе сущности можно создавать и редактировать в одном экране.
4.3.5. Использование независимых полей вместо FieldGroup
Использование компонента FieldGroup в редакторе сущности не является обязательным. Мы можете легко заменить FieldGroup на отдельные поля, чтобы кастомизировать стандартную разметку экрана.
Ниже приведён пример такого экрана для редактирования сущности Order.
XML-дескриптор экрана order-edit.xml содержит основной источник данных orderDs и вложенный в него orderLinesDs для строк заказа, а также независимый источник данных customersDs, чтобы загружать список покупателей для выбора из выпадающего списка lookupField:
<dsContext>
<datasource id="orderDs"
class="com.company.sample.entity.Order"
view="order-edit">
<collectionDatasource id="orderLinesDs"
property="orderLines"/>
</datasource>
<collectionDatasource id="customersDs"
class="com.company.sample.entity.Customer"
view="_minimal">
<query>
<![CDATA[select e from sample$Customer e]]>
</query>
</collectionDatasource>
</dsContext>
Теперь достаточно создать поля для всех атрибутов сущности Order, указать для каждого поля соответствующий источник данных и нужный атрибут сущности, используя XML-атрибут property:
<textField id="numField"
caption="msg://order.num"
datasource="orderDs"
property="num"/>
<lookupField id="customerField"
caption="msg://order.customer"
datasource="orderDs"
property="customer"
optionsDatasource="customersDs"/>
<datePicker id="datePicker"
caption="msg://order.date"
datasource="orderDs"
property="date"/>
<table id="orderLinesTable"
height="300px"
width="100%">
<rows datasource="orderLinesDs"/>
. . .
</table>
Такой подход к созданию экранов предоставляет большую гибкость в использовании визуальных компонентов и их расположении на экране.
Данный раздел посвящен использованию визуальных тем в веб-приложениях.
4.3.6.1. Создание темы Hover Dark
В этой главе мы рассмотрим создание тёмной вариации стандартной темы Hover - Hover Dark. Пример приложения, в котором использована эта тема, доступен на GitHub.
Требуемая структура файлов темы будет автоматически создана в модуле web. Модуль webThemesModule и его конфигурация будут автоматически добавлены в скрипты settings.gradle и build.gradle.
@import "../hover/hover-defaults";
$v-app-background-color: #262626;
$v-background-color: lighten($v-app-background-color, 12%);
$v-border: 1px solid (v-tint 0.8);
$font-color: valo-font-color($v-background-color, 0.85);
$v-button-font-color: $font-color;
$v-font-color: $font-color;
$v-link-font-color: lighten($v-focus-color, 15%);
$v-link-text-decoration: none;
$v-textfield-background-color: $v-background-color;
$cuba-hover-color: #75a4c1;
$cuba-maintabsheet-tabcontainer-background-color: $v-app-background-color;
$cuba-menubar-background-color: lighten($v-app-background-color, 4%);
$cuba-tabsheet-tab-caption-selected-color: $v-font-color;
$cuba-window-modal-header-background: $v-background-color;
$cuba-menubar-menuitem-border-radius: 0;
cuba.themeConfig = hover-theme.properties /com/company/demo/web/hover-dark-theme.properties
В результате, в приложении будут доступны две темы - стандартная тема Hover и её тёмная вариация.
4.3.6.2. Создание темы Facebook
Рассмотрим пример создания на основе Halo новой темы Facebook, напоминающей интерфейс сайта известной социальной сети.
themes/
facebook/
branding/
app-icon-login.png
app-icon-menu.png
com.haulmont.cuba/
app-component.scss // cuba app-component include
facebook.scss // main theme file
facebook-defaults.scss // main theme variables
favicon.ico
styles.scss // entry point of SCSS build procedure
Файл styles.scss содержит список ваших тем:
@import "facebook-defaults";
@import "facebook";
.facebook {
@include facebook;
}
Содержимое файла facebook.scss:
@import "../halo/halo";
@mixin facebook {
@include halo;
}
Содержимое файла app-component.scss из каталога com.haulmont.cuba:
@import "../facebook";
@mixin com_haulmont_cuba {
@include facebook;
}
@import "../halo/halo-defaults";
$v-background-color: #fafafa;
$v-app-background-color: #e7ebf2;
$v-panel-background-color: #fff;
$v-focus-color: #3b5998;
$v-border-radius: 0;
$v-textfield-border-radius: 0;
$v-font-family: Helvetica, Arial, 'lucida grande', tahoma, verdana, arial, sans-serif;
$v-font-size: 14px;
$v-font-color: #37404E;
$v-font-weight: 400;
$v-link-text-decoration: none;
$v-shadow: 0 1px 0 (v-shade 0.2);
$v-bevel: inset 0 1px 0 v-tint;
$v-unit-size: 30px;
$v-gradient: v-linear 12%;
$v-overlay-shadow: 0 3px 8px v-shade, 0 0 0 1px (v-shade 0.7);
$v-shadow-opacity: 20%;
$v-selection-overlay-padding-horizontal: 0;
$v-selection-overlay-padding-vertical: 6px;
$v-selection-item-border-radius: 0;
$v-line-height: 1.35;
$v-font-size: 14px;
$v-font-weight: 400;
$v-unit-size: 25px;
$v-font-size--h1: 22px;
$v-font-size--h2: 18px;
$v-font-size--h3: 16px;
$v-layout-margin-top: 8px;
$v-layout-margin-left: 8px;
$v-layout-margin-right: 8px;
$v-layout-margin-bottom: 8px;
$v-layout-spacing-vertical: 8px;
$v-layout-spacing-horizontal: 8px;
$v-table-row-height: 25px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 5px;
$v-focus-style: inset 0px 0px 1px 1px rgba($v-focus-color, 0.5);
$v-error-focus-style: inset 0px 0px 1px 1px rgba($v-error-indicator-color, 0.5);
cuba.web.theme = facebook
cuba.themeConfig = havana-theme.properties halo-theme.properties /com/company/application/web/facebook-theme.properties
Свойство приложения cuba.themeConfig определяет, какие темы будут доступны в меню приложения Settings.
4.3.6.3. Переход с темы Havana на полнофункциональную Halo
Тема Halo лучше поддаётся расширению, в ней поддерживаются новые визуальные компоненты, такие как DataGrid и SideMenu. Если вы хотите использовать эти компоненты, а также получать обновления библиотеки визуальных компонентов, рекомендуется использовать тему Halo. В то же время, если вам важно сохранить внешний вид темы Havana, вы можете использовать следующие переменные в файле halo-ext-defaults.scss:
$cuba-menubar-background-color: #315379;
$cuba-menubar-border-color: #315379;
$v-table-row-height: 25px;
$v-selection-color: rgb(77, 122, 178);
$v-table-header-font-size: 12px;
$v-textfield-border: 1px solid #A5C4E0;
$v-selection-item-selection-color: #4D7AB2;
$v-app-background-color: #E3EAF1;
$v-font-size: 12px;
$v-font-weight: 400;
$v-unit-size: 25px;
$v-border-radius: 0px;
$v-border: 1px solid #9BB3D3 !default;
$v-font-family: Verdana,tahoma,arial,geneva,helvetica,sans-serif,"Trebuchet MS";
$v-panel-background-color: #ffffff;
$v-background-color: #ffffff;
$cuba-menubar-menuitem-text-color: #ffffff;
$cuba-app-menubar-padding-top: 8px;
$cuba-app-menubar-padding-bottom: 8px;
$cuba-menubar-text-color: #ffffff;
$cuba-menubar-submenu-padding: 1px;
4.4. Загрузка и сохранение данных
В данном разделе рассматриваются различные способы загрузки и сохранения данных в БД.
4.4.1. DataManager vs. EntityManager
И DataManager и EntityManager предназначены для выполнения операций с сущностями (CRUD). Ниже приведены различия между этими интерфейсами.
|
DataManager |
EntityManager |
|
DataManager доступен и на среднем слое и на клиентском уровне. |
EntityManager доступен только на среднем слое. |
|
DataManager является синглтон-бином. |
Ссылку на EntityManager необходимо получать через интерфейс Persistence. |
|
DataManager содержит несколько высокоуровневых методов для работы с detached сущностями: load(), loadList(), reload(), commit(). |
EntityManager в большой степени повторяет стандартный javax.persistence.EntityManager. |
DataManager на самом деле делегирует выполнение реализациям DataStore, поэтому особенности DataManager, перечисленные ниже, актуальны только для наиболее часто встречающегося случая, когда вы работаете с сущностями, хранящимися в реляционной базе данных.
|
DataManager |
EntityManager |
|
DataManager всегда стартует новую транзакцию внутри. |
Для работы с EntityManager необходима открытая транзакция. |
|
DataManager загружает частичные сущности в соответствие с представлением. Есть некоторые исключения, см. подробности. |
EntityManager всегда загружает все локальные атрибуты. Если используется представление, оно влияет только на загрузку ссылочных атрибутов. См. подробности. |
|
DataManager выполняет только JPQL запросы. Кроме того, он имеет отдельные методы для загрузки сущностей: load(), loadList(); и скалярных и агрегатных значений: loadValues(). |
EntityManager может выполнять любые JPQL или native (SQL) запросы. |
|
DataManager проверяет права доступа, когда вызывается с клиентского уровня. |
EntityManager не проверяет права доступа. |
При работе на клиентском уровне доступен только DataManager. На среднем слое, используйте EntityManager когда необходимо реализовать атомарную логику внутри транзакции или если его интерфейс лучше подходит для решения задачи. В противном случае, на среднем слое можно использовать любой из интерфейсов на выбор.
Если вам нужно обойти ограничения DataManager при работе на клиентском уровне, создайте свой сервис и используйте EntityManager для работы с данными. В сервисе можно проверять права пользователя с помощью интерфейса Security и возвращать клиенту данные в виде персистентных или неперсистентных сущностей или произвольных значений.
Данный раздел содержит ряд примеров использования REST API.
Детальная информация о методах REST API описана согласно спецификации Swagger и доступна по адресу http://files.cuba-platform.com/swagger/6.10.
OAuth токен необходим для выполнения любого метода REST API (кроме случая анонимного доступа к REST API). Получить токен можно выполнив POST запрос по адресу
http://localhost:8080/app/rest/v2/oauth/token
Доступ к данному URL защищен с помощью базовой аутентификации с использованием идентификатора и пароля клиента REST API. Обратите внимание, что это не логин и пароль пользователя приложения. Идентификатор и пароль клиента REST API заданы в свойствах приложения cuba.rest.client.id и cuba.rest.client.secret (значения по умолчанию для этих свойств: client и secret). Заголовок Authorization запроса на получение токена должен содержать логин и пароль клиента, разделенные символом ":" и закодированные в base64.
Тип запроса на получение токена должен быть application/x-www-form-urlencoded, кодировка UTF-8.
Запрос должен содержать следующие параметры:
POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=smith&password=qwerty123
Вы также можете использовать утилиту cURL:
curl -H "Content-type: application/x-www-form-urlencoded" -H "Authorization: Basic Y2xpZW50OnNlY3JldA==" -d "grant_type=password&username=admin&password=admin" http://localhost:8080/app/rest/v2/oauth/token
Метод возвращает JSON объект:
{
"access_token": "29bc6b45-83cd-4050-8c7a-2a8a60adf251",
"token_type": "bearer",
"refresh_token": "e765446f-d49e-4634-a6d3-2d0583a0e7ea",
"expires_in": 43198,
"scope": "rest-api"
}
Значение токена содержится в поле access_token
Чтобы использовать токен, его нужно передать в заголовке запроса Authorization с типом Bearer, например:
Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251
Свойство refresh_token содержит значение refresh токена. Он не может быть использован для доступа к защищённым ресурсам приложения, но он имеет большее время жизни по сравнению с access токеном и используется для получения нового access токена, когда старый токен истек.
Запрос на получение нового access токена с помощью refresh токена должен содержать следующие параметры:
POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=e765446f-d49e-4634-a6d3-2d0583a0e7ea
См. также следующие свойства приложения, имеющие отношение к токенам:
4.5.2. Аутентификация LDAP в REST API
Для настройки LDAP аутентификации в REST API используются следующие свойства приложения:
Пример содержимого файла local.app.properties:
cuba.rest.ldap.enabled = true
cuba.rest.ldap.urls = ldap://192.168.1.1:389
cuba.rest.ldap.base = ou=Employees,dc=mycompany,dc=com
cuba.rest.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com
cuba.rest.ldap.password = system_user_password
Получить OAuth токен можно выполнив POST запрос по адресу:
http://localhost:8080/app/rest/v2/ldap/token
Доступ к данному URL защищен с помощью базовой аутентификации с использованием идентификатора и пароля клиента REST API. Обратите внимание, что это не логин и пароль пользователя приложения. Идентификатор и пароль клиента REST API заданы в свойствах приложения cuba.rest.client.id и cuba.rest.client.secret (значения по умолчанию для этих свойств: client и secret). Заголовок Authorization запроса на получение токена должен содержать логин и пароль клиента, разделенные символом ":" и закодированные в base64.
Запрос должен содержать следующие параметры (соответствуют параметрам стандартной аутентификации):
Тип запроса на получение токена должен быть application/x-www-form-urlencoded, кодировка UTF-8.
Стандартная аутентификация может быть отключена при помощи свойства cuba.rest.standardAuthenticationEnabled:
cuba.rest.standardAuthenticationEnabled = false
4.5.3. Собственный механизм аутентификации
Различные механизмы аутентификации могут предоставлять токен по ключу, по ссылке, по логину и паролю LDAP и т.д. Стандартный механизм аутентификации в REST API изменить нельзя, но можно создать свой механизм. Для этого необходимо создать REST-контроллер, который предоставит свой URL для входа в приложение.
В этом примере мы рассмотрим механизм аутентификации, позволяющий получить OAuth-токен по промо-коду. За основу возьмём приложение, содержащее сущность Coupon (Купон) с атрибутом code (промо-код). Значение этого атрибута мы будем передавать в качестве параметра аутентификации в GET-запросе.
@Column(name = "CODE", unique = true, length = 4)
protected String code;
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<context:component-scan base-package="com.company.demo.web.rest"/>
</beans>
cuba.restSpringContextConfig = +com/company/demo/rest-dispatcher-spring.xml
@RestController
@RequestMapping("auth-code")
public class AuthCodeController {
@Inject
private OAuthTokenIssuer oAuthTokenIssuer;
@Inject
private LoginService loginService;
@Inject
private Configuration configuration;
@Inject
private DataManager dataManager;
@Inject
private MessageTools messageTools;
// here we check secret code and issue token using OAuthTokenIssuer
@RequestMapping(method = RequestMethod.GET)
public ResponseEntity get(@RequestParam("code") String authCode) {
// obtain system session to be able to call middleware services
WebAuthConfig webAuthConfig = configuration.getConfig(WebAuthConfig.class);
UserSession systemSession;
try {
systemSession = loginService.getSystemSession(webAuthConfig.getTrustedClientPassword());
} catch (LoginException e) {
throw new RuntimeException("Error during system auth");
}
// set security context
AppContext.setSecurityContext(new SecurityContext(systemSession));
try {
// find coupon with code
LoadContext<Coupon> loadContext = LoadContext.create(Coupon.class)
.setQuery(LoadContext.createQuery("select c from demo$Coupon c where c.code = :code")
.setParameter("code", authCode));
if (dataManager.load(loadContext) == null) {
// if coupon is not found - code is incorrect
return new ResponseEntity<>(new ErrorInfo("invalid_grant", "Bad credentials"), HttpStatus.BAD_REQUEST);
}
// generate token for "promo-user"
OAuthTokenIssuer.OAuth2AccessTokenResult tokenResult =
oAuthTokenIssuer.issueToken("promo-user", messageTools.getDefaultLocale(), Collections.emptyMap());
OAuth2AccessToken accessToken = tokenResult.getAccessToken();
// set security HTTP headers to prevent browser caching of security token
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
headers.set(HttpHeaders.PRAGMA, "no-cache");
return new ResponseEntity<>(accessToken, headers, HttpStatus.OK);
} finally {
// clean up security context
AppContext.setSecurityContext(null);
}
}
// POJO for JSON error messages
public static class ErrorInfo implements Serializable {
private String error;
private String error_description;
public ErrorInfo(String error, String error_description) {
this.error = error;
this.error_description = error_description;
}
public String getError() {
return error;
}
public String getError_description() {
return error_description;
}
}
}
<context:component-scan base-package="com.company.demo">
<context:exclude-filter type="regex" expression="com\.company\.demo\.web\.rest\..*"/>
</context:component-scan>
http://localhost:8080/app/rest/auth-code?code=A325
Результат:
{"access_token":"74202587-6c2b-4d74-bcf2-0d687ea85dca","token_type":"bearer","expires_in":43199,"scope":"rest-api"}
Теперь полученный access token нужно передавать в REST API, как описано в общей документации.
4.5.3.1. Social Login в REST API
Механизм авторизации через социальные сети, или social login, также можно использовать в REST API. Исходный код приложения, описанного в этом примере, доступен на GitHub, а сам пример описан в разделе Social Login. Ниже приведены ключевые моменты этого примера, позволяющие получить OAuth-токен через аккаунт Facebook.
FacebookAuthenticationController.java
@RequestMapping(method = RequestMethod.GET)
public ResponseEntity get() {
String loginUrl = getAsPrivilegedUser(() ->
facebookService.getLoginUrl(getAppUrl(), OAuth2ResponseType.CODE_TOKEN)
);
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.LOCATION, loginUrl);
return new ResponseEntity<>(headers, HttpStatus.FOUND);
}
Здесь мы проверяем переданный код Facebook, получаем код доступа и издаём токен с помощью OAuthTokenIssuer:
FacebookAuthenticationController.java
@RequestMapping(method = RequestMethod.POST, value = "login")
public ResponseEntity<OAuth2AccessToken> login(@RequestParam("code") String code) {
User user = getAsPrivilegedUser(() -> {
FacebookUserData userData = facebookService.getUserData(getAppUrl(), code);
return socialRegistrationService.findOrRegisterUser(
userData.getId(), userData.getEmail(), userData.getName());
});
OAuth2AccessTokenResult tokenResult = oAuthTokenIssuer.issueToken(user.getLogin(),
messageTools.getDefaultLocale(), Collections.emptyMap());
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
headers.set(HttpHeaders.PRAGMA, "no-cache");
return new ResponseEntity<>(tokenResult.getAccessToken(), headers, HttpStatus.OK);
}
<context:component-scan base-package="com.company.demo">
<context:exclude-filter type="regex" expression="com\.company\.demo\.restapi\..*"/>
</context:component-scan>
<html>
<head>
<title>Facebook login demo with REST-API</title>
<script src="jquery-3.2.1.min.js"></script>
<style type="text/css">
#users { display: none; }
</style>
</head>
<body>
<h1>Facebook login demo with REST-API</h1>
<script type="application/javascript"...>
</script>
<a id="fbLink" href="/app/rest/facebook">Login with Facebook</a>
<div id="users">
You are logged in!
<h1>Users</h1>
<div id="usersList">
</div>
</div>
</body>
</html>
В этом скрипте мы попробуем залогиниться через Facebook. Сначала удаляем лишний код из URL, затем передаём код в REST API для получения OAuth-токена, и в случае успешной аутентификации мы сможем загружать и сохранять данные как обычно:
var oauth2Token = null;
function tryToLoginWithFacebook() {
var urlHash = window.location.hash;
if (urlHash && urlHash.indexOf('&code=') >= 0) {
console.log("Try to login to CUBA REST-API!");
var urlCode = urlHash.substring(urlHash.indexOf('&code=') + '&code='.length);
console.log("Facebook code: " + urlCode);
history.pushState("", document.title, window.location.pathname);
$.post({
url: '/app/rest/facebook/login',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
dataType: 'json',
data: {code: urlCode},
success: function (data) {
oauth2Token = data.access_token;
loadUsers();
}
})
}
}
function loadUsers() {
$.get({
url: '/app/rest/v2/entities/sec$User?view=_local',
headers: {
'Authorization': 'Bearer ' + oauth2Token,
'Content-Type': 'application/x-www-form-urlencoded'
},
success: function (data) {
$('#fbLink').hide();
$('#users').show();
$.each(data, function (i, user) {
$('#usersList').append("<li>" + user.name + " (" + user.email + ")</li>");
});
}
});
}
tryToLoginWithFacebook();
Другой пример использования JavaScript-кода в приложениях CUBA вы можете найти в разделе Пример использования из JavaScript.
4.5.4. Получение списка экземпляров сущности
Предположим, в системе имеется сущность sales$Order, и необходимо получить список экземпляров этой сущности. При этом, необходимо получить не все записи, а 50 записей, начиная с сотой (для отображения третьей странице в каком-либо списке клиентского приложения). Кроме простых атрибутов сущности sales$Order результат должен содержать данные о клиенте (поле customer). Заказы должны быть отсортированы по дате.
Базовый URL для получения списка экземпляров сущности sales$Order:
http://localhost:8080/app/rest/v2/entities/sales$Order
Для выполнения описанных выше условий необходимо задать параметры запроса:
OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer:
Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251
В итоге получаем следующий GET http-запрос:
http://localhost:8080/app/rest/v2/entities/sales$Order?view=order-edit-view&limit=50&offset=100&sort=date
В cURL запрос может выглядеть так:
curl -H "Authorization: Bearer d335902c-9cb4-455e-bf92-24ca1d66d72f" http://localhost:8080/app/rest/v2/entities/sales$Order?view=order-edit&limit=50&offset=100&sort=date
Ответ будет выглядеть следующим образом:
[
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"date": "2016-10-31",
"description": "Vacation order",
"number": "00001",
"items": [
{
"_entityName": "sales$OrderItem",
"_instanceName": "Beach umbrella",
"id": "95a04f46-af7a-a307-de4e-f2d73cfc74f7",
"price": 23,
"name": "Beach umbrella"
},
{
"_entityName": "sales$OrderItem",
"_instanceName": "Sun lotion",
"id": "a2129675-d158-9e3a-5496-41bf1a315917",
"price": 9.9,
"name": "Sun lotion"
}
],
"customer": {
"_entityName": "sales$Customer",
"_instanceName": "Toby Burns",
"id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05",
"firstName": "Toby",
"lastName": "Burns"
}
},
{
"_entityName": "sales$Order",
"_instanceName": "00002",
"id": "b2ad3059-384c-3e03-b62d-b8c76621b4a8",
"date": "2016-12-31",
"description": "New Year party set",
"number": "00002",
"items": [
{
"_entityName": "sales$OrderItem",
"_instanceName": "Jack Daniels",
"id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe",
"price": 50.7,
"name": "Jack Daniels"
},
{
"_entityName": "sales$OrderItem",
"_instanceName": "Hennessy X.O",
"id": "c01be87b-3f91-7a86-50b5-30f2f0a49127",
"price": 79.9,
"name": "Hennessy X.O"
}
],
"customer": {
"_entityName": "sales$Customer",
"_instanceName": "Morgan Collins",
"id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
"firstName": "Morgan",
"lastName": "Collins"
}
}
]
Обратите внимание, что для каждой сущности загружаются атрибуты _entityName с именем сущности и _instanceName, содержащий результат вычисления короткого имени для сущности.
4.5.5. Создание экземпляра сущности
Для создания нового экземпляра сущности sales$Order необходимо выполнить POST запрос по адресу:
http://localhost:8080/app/rest/v2/entities/sales$Order
OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.
Тело запроса должно содержать JSON объект, описывающий новый экземпляр, например:
{
"number": "00017",
"date": "2016-09-01",
"description": "Back to school",
"items": [
{
"_entityName": "sales$OrderItem",
"price": 100,
"name": "School bag"
},
{
"_entityName": "sales$OrderItem",
"price": 9.90,
"name": "Pencils"
}
],
"customer": {
"id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05"
}
}
Ниже приведен пример POST запроса для создания нового экземпляра Order в cURL:
curl -H "Authorization: Bearer d335902c-9cb4-455e-bf92-24ca1d66d72f" -H "Content-Type: application/json" -X POST -d "{\"date\": \"2018-10-12 15:47:28\", \"amount\": 9.90, \"customer\": {\"id\": \"383ebce2-b295-7378-36a1-bcf93693821f\"}}" http://localhost:8080/app/rest/v2/entities/sales$Order
В теле запроса передается коллекция позиций заказа items и ссылка на клиента customer. Рассмотрим, как будут обработаны эти атрибуты.
Сначала посмотрим на класс Order:
package com.company.sales.entity;
import com.haulmont.chile.core.annotations.Composition;
import com.haulmont.chile.core.annotations.NamePattern;
import com.haulmont.cuba.core.entity.StandardEntity;
import com.haulmont.cuba.core.entity.annotation.OnDelete;
import com.haulmont.cuba.core.global.DeletePolicy;
import javax.persistence.*;
import java.util.Date;
import java.util.Set;
@NamePattern("%s|number")
@Table(name = "SALES_ORDER")
@Entity(name = "sales$Order")
public class Order extends StandardEntity {
private static final long serialVersionUID = 7565070704618724997L;
@Column(name = "NUMBER_")
protected String number;
@Temporal(TemporalType.DATE)
@Column(name = "DATE_")
protected Date date;
@Column(name = "DESCRIPTION")
protected String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@Composition
@OnDelete(DeletePolicy.CASCADE)
@OneToMany(mappedBy = "order")
protected Set<OrderItem> items;
//getters and setters omitted
}
В сущности Order коллекция items аннотирована @Composition. Методы создания и обновления сущности REST API создают новые экземпляры для всех элементов таких коллекций. Т.е. вместе с заказом (Order) будет создано две позиции заказа (OrderItem).
Ссылка customer не имеет аннотации @Composition, поэтому метод REST API попытается найти клиента с переданным идентификатором и проставить его в поле customer. Если клиент не будет найден, заказ не будет создан и метод вернет ошибку.
В случае успеха возвращается полный граф созданной сущности:
{
"_entityName": "sales$Order",
"id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50",
"date": "2016-09-01",
"description": "Back to school",
"version": 1,
"number": "00017",
"createdBy": "admin",
"createTs": "2016-10-13 18:12:21.047",
"updateTs": "2016-10-13 18:12:21.047",
"items": [
{
"_entityName": "sales$OrderItem",
"id": "3158b8ed-7b7a-568e-aec5-0822c3ebbc24",
"createdBy": "admin",
"price": 9.9,
"name": "Pencils",
"createTs": "2016-10-13 18:12:21.047",
"version": 1,
"updateTs": "2016-10-13 18:12:21.047",
"order": {
"_entityName": "sales$Order",
"id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50"
}
},
{
"_entityName": "sales$OrderItem",
"id": "72774b8b-4fea-6403-7b52-4a6a749215fc",
"createdBy": "admin",
"price": 100,
"name": "School bag",
"createTs": "2016-10-13 18:12:21.047",
"version": 1,
"updateTs": "2016-10-13 18:12:21.047",
"order": {
"_entityName": "sales$Order",
"id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50"
}
}
],
"customer": {
"_entityName": "sales$Customer",
"id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05",
"firstName": "Toby",
"lastName": "Burns",
"createdBy": "admin",
"createTs": "2016-10-13 15:32:01.657",
"version": 1,
"updateTs": "2016-10-13 15:32:01.657"
}
}
4.5.6. Изменение существующего экземпляра сущности
Для изменения экземпляра сущности sales$Order необходимо выполнить PUT запрос по адресу:
http://localhost:8080/app/rest/v2/entities/sales$Order/5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50
Последняя часть запроса здесь - это идентификатор изменяемой сущности.
OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.
В теле запроса необходимо передать JSON объект, содержащий только поля, которые мы хотим изменить, например:
{
"date": "2017-10-01",
"customer" : {
"id" : "5d111245-2ed0-abec-3bee-1a196da92e3e"
}
}
В теле ответа будет возвращена измененная сущность:
{
"_entityName": "sales$Order",
"id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50",
"date": "2017-10-01",
"updatedBy": "admin",
"description": "Back to school",
"version": 2,
"number": "00017",
"createdBy": "admin",
"createTs": "2016-10-13 18:12:21.047",
"updateTs": "2016-10-13 19:13:02.656",
"customer": {
"_entityName": "sales$Customer",
"id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
"firstName": "Morgan",
"lastName": "Collins",
"createdBy": "admin",
"createTs": "2016-10-13 15:31:27.821",
"version": 1,
"updateTs": "2016-10-13 15:31:27.821",
"email": "collins@gmail.com"
}
}
4.5.7. Выполнение JPQL-запроса (GET)
Перед выполнением запроса с помощью REST API необходимо описать его в конфигурационном файле. В корневом пакете модуля web (например, com.company.sales) необходимо создать файл rest-queries.xml. Затем этот файл объявляется в файле свойств приложения модуля web (web-app.properties):
cuba.rest.queriesConfig = +com/company/sales/rest-queries.xml
Содержимое файла rest-queries.xml:
<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
<query name="ordersAfterDate" entity="sales$Order" view="order-edit-view">
<jpql><![CDATA[select o from sales$Order o where o.date >= :startDate and o.date <= :endDate]]></jpql>
<params>
<param name="startDate" type="java.util.Date"/>
<param name="endDate" type="java.util.Date"/>
</params>
</query>
</queries>
Для выполнения JPQL запроса, необходимо выполнить GET http-запрос к REST API:
http://localhost:8080/app/rest/v2/queries/sales$Order/ordersAfterDate?startDate=2016-11-01&endDate=2017-11-01
Части URL:
OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.
Метод возвращает JSON-массив со списком извлеченных экземпляров сущности:
[
{
"_entityName": "sales$Order",
"_instanceName": "00002",
"id": "b2ad3059-384c-3e03-b62d-b8c76621b4a8",
"date": "2016-12-31",
"description": "New Year party set",
"number": "00002",
"items": [
{
"_entityName": "sales$OrderItem",
"_instanceName": "Jack Daniels",
"id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe",
"price": 50.7,
"name": "Jack Daniels"
},
{
"_entityName": "sales$OrderItem",
"_instanceName": "Hennessy X.O",
"id": "c01be87b-3f91-7a86-50b5-30f2f0a49127",
"price": 79.9,
"name": "Hennessy X.O"
}
],
"customer": {
"_entityName": "sales$Customer",
"_instanceName": "Morgan Collins",
"id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
"firstName": "Morgan",
"lastName": "Collins"
}
}
]
Список других возможных параметров для метода выполнения запросов можно посмотреть в Swagger документации.
4.5.8. Выполнение JPQL-запроса (POST)
JPQL-запрос также может быть выполнен с помощью POST-запроса. Это необходимо для случая, когда параметр JPQL-запроса является коллекцией. В файле конфигурации JPQL-запросов для REST API тип параметра-коллекции должен заканчиваться символами []: java.lang.String[], java.util.UUID[] и т.п.
<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
<query name="ordersByIds" entity="sales$Order" view="order-edit-view">
<jpql><![CDATA[select o from sales$Order o where o.id in :ids and o.status = :status]]></jpql>
<params>
<param name="ids" type="java.util.UUID[]"/>
<param name="status" type="java.lang.String"/>
</params>
</query>
</queries>
Параметры JPQL-запроса должны быть переданы в теле HTTP-запроса в JSON map:
{
"ids": ["c273fca1-33c2-0229-2a0c-78bc6d09110a", "e6c04c18-c8a1-b741-7363-a2d58589d800", "d268a4e1-f316-a7c8-7a96-87ba06afbbbd"],
"status": "ready"
}
URL POST-запроса:
http://localhost:8080/app/rest/v2/queries/sales$Order/ordersByIds?returnCount=true
4.5.9. Вызов метода сервиса (GET)
Предположим, в системе имеется сервис OrderService, реализация которого выглядит следующим образом:
package com.company.sales.service;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.Transaction;
import org.springframework.stereotype.Service;
import javax.inject.Inject;
import java.math.BigDecimal;
@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {
@Inject
private Persistence persistence;
@Override
public BigDecimal calculatePrice(String orderNumber) {
BigDecimal orderPrice = null;
try (Transaction tx = persistence.createTransaction()) {
EntityManager em = persistence.getEntityManager();
orderPrice = (BigDecimal) em.createQuery("select sum(oi.price) from sales$OrderItem oi where oi.order.number = :orderNumber")
.setParameter("orderNumber", orderNumber)
.getSingleResult();
tx.commit();
}
return orderPrice;
}
}
Перед выполнением метода сервиса с помощью REST API необходимо разрешить его вызов в конфигурационном файле. В корневом пакете модуля web (например, com.company.sales) необходимо создать файл rest-services.xml. Затем этот файл объявляется в файле свойств приложения модуля web (web-app.properties):
cuba.rest.servicesConfig = +com/company/sales/rest-services.xml
Содержимое файла rest-services.xml:
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
<service name="sales_OrderService">
<method name="calculatePrice">
<param name="orderNumber"/>
</method>
</service>
</services>
Для вызова метода сервиса, необходимо выполнить GET http-запрос к REST API вида:
http://localhost:8080/app/rest/v2/services/sales_OrderService/calculatePrice?orderNumber=00001
Части URL:
OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.
Метод сервиса может вернуть как простой тип данных, так и сущность, коллекцию сущностей или произвольный POJO. В нашем случае метод возвращает BigDecimal, поэтому в теле ответа нам вернется число:
39.2
4.5.10. Вызов метода сервиса (POST)
REST API позволяет выполнять методы сервисов, аргументами которых являются не только простые типы, но также:
Небольшой пример. Предположим, в сервис OrderService, созданный в предыдущем разделе, добавлен следующий метод:
@Override
public OrderValidationResult validateOrder(Order order, Date validationDate){
OrderValidationResult result=new OrderValidationResult();
result.setSuccess(false);
result.setErrorMessage("Validation of order "+order.getNumber()+" failed. validationDate parameter is: "+validationDate);
return result;
}
Класс OrderValidationResult выглядит следующим образом:
package com.company.sales.service;
import java.io.Serializable;
public class OrderValidationResult implements Serializable {
private boolean success;
private String errorMessage;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
Новый метод сервиса принимает сущность Order в качестве первого аргумента и возвращает POJO.
Перед вызовом данного метода с помощью REST API необходимо разрешить его, добавив запись в конфигурационный файл rest-services.xml (его создание было рассмотрено в Вызов метода сервиса (GET)):
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
<service name="sales_OrderService">
<method name="calculatePrice">
<param name="orderNumber"/>
</method>
<method name="validateOrder">
<param name="order"/>
<param name="validationDate"/>
</method>
</service>
</services>
Метод validateOrder сервиса вызывается POST запросом по адресу:
http://localhost:8080/app/rest/v2/services/sales_OrderService/validateOrder
Параметры в случае POST передаются в теле запроса в JSON объекте. Каждое поле JSON объекта соответствует аргументу метода сервиса:
{
"order" : {
"number": "00050",
"date" : "2016-01-01"
},
"validationDate": "2016-10-01"
}
OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.
Метод вернет сериализованный POJO:
{
"success": false,
"errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01"
}
При скачивании файла передавать токен в заголовке запроса часто оказывается неудобно. Хочется иметь URL для скачивания, который можно подставить например в атрибут src тега img. В этом случае передача <<OAuth-токена возможна в самом запросе в параметре с именем access_token.
Например, в систему загружено изображение. Идентификатор соответствующего FileDescriptor - 44809679-e81c-e5ae-dd81-f56f223761d6.
В этом случае URL для загрузки изображения будет выглядеть так:
http://localhost:8080/app/rest/v2/files/44809679-e81c-e5ae-dd81-f56f223761d6?access_token=a2f0bb4e-773f-6b59-3450-3934cbf0a2d6
Для загрузки файлов вам нужно получить OAuth-токен, который понадобится в дальнейших запросах.
Используем простую форму для загрузки:
<form id="fileForm">
<h2>Select a file:</h2>
<input type="file" name="file" id="fileUpload"/>
<br/>
<button type="submit">Upload</button>
</form>
<h2>Result:</h2>
<img id="uploadedFile" src="" style="display: none"/>
Далее используем jQuery, чтобы получить JSON с объектом data, который, по сути, представляет собой новый FileDescriptor. Использовать загруженный файл мы можем по идентификатору созданного экземпляра FileDescriptor с токеном в качестве параметра:
$('#fileForm').submit(function (e) {
e.preventDefault();
var file = $('#fileUpload')[0].files[0];
var url = 'http://localhost:8080/app/rest/v2/files?name=' + file.name; // send file name as parameter
$.ajax({
type: 'POST',
url: url,
headers: {
'Authorization': 'Bearer ' + oauthToken // add header with access token
},
processData: false,
contentType: false,
dataType: 'json',
data: file,
success: function (data) {
alert('Upload successful');
$('#uploadedFile').attr('src',
'http://localhost:8080/app/rest/v2/files/' + data.id + '?access_token=' + oauthToken); // update image url
$('#uploadedFile').show();
}
});
});
4.5.13. Пример использования из JavaScript
В данном разделе приведен пример использования REST API v2 из JavaScript, выполняющегося на HTML-странице. На странице изначально отображается форма логина, а после успешного входа - соответствующее сообщение и список сущностей.
Для простоты, в данном примере для хранения файлов HTML/CSS/JavaScript используется каталог modules/web/web/VAADIN, так как соответствующий каталог развернутого веб-приложения используется для выдачи статических ресурсов. Поэтому никаких настроек на сервере Tomcat делать не требуется. Результирующий URL будет начинаться с http://localhost:8080/app/VAADIN, так что данный вариант не стоит использовать в реальном приложении, вместо этого создайте отдельное веб-приложение со своим контекстом.
Загрузите jQuery и Bootstrap и скопируйте в каталог modules/web/web/VAADIN вашего проекта. Создайте файлы customers.html и customers.js, так что содержимое каталога должно быть примерно таким:
bootstrap.min.css
customers.html
customers.js
jquery-3.1.1.min.js
Содержимое файла customers.html:
<html>
<head>
<script type="text/javascript" src="jquery-3.1.1.min.js"></script>
<link rel="stylesheet" href="bootstrap.min.css"/>
</head>
<body>
<div style="width: 300px; margin: auto;">
<h1>Sales</h1>
<div id="loggedInStatus" style="display: none" class="alert alert-success">
Logged in successfully
</div>
<div id="loginForm">
<div class="form-group">
<label for="loginField">Login:</label>
<input type="text" class="form-control" id="loginField">
</div>
<div class="form-group">
<label for="passwordField">Password:</label>
<input type="password" class="form-control" id="passwordField">
</div>
<button type="submit" class="btn btn-default" onclick="login()">Submit</button>
</div>
<div id="customers" style="display: none">
<h2>Customers</h2>
<ul id="customersList"></ul>
</div>
</div>
<script type="text/javascript" src="customers.js"></script>
</body>
</html>
Содержимое файла customers.js:
var oauthToken = null;
function login() {
var userLogin = $('#loginField').val();
var userPassword = $('#passwordField').val();
$.post({
url: 'http://localhost:8080/app/rest/v2/oauth/token',
headers: {
'Authorization': 'Basic Y2xpZW50OnNlY3JldA==',
'Content-Type': 'application/x-www-form-urlencoded'
},
dataType: 'json',
data: {grant_type: 'password', username: userLogin, password: userPassword},
success: function (data) {
oauthToken = data.access_token;
$('#loggedInStatus').show();
$('#loginForm').hide();
loadCustomers();
}
})
}
function loadCustomers() {
$.get({
url: 'http://localhost:8080/app/rest/v2/entities/sales$Customer?view=_local',
headers: {
'Authorization': 'Bearer ' + oauthToken,
'Content-Type': 'application/x-www-form-urlencoded'
},
success: function (data) {
$('#customers').show();
$.each(data, function (i, customer) {
$('#customersList').append("<li>" + customer.name + " (" + customer.email + ")</li>");
});
}
});
}
Имя пользователя и пароль из полей ввода передаётся на сервер POST-запросом с закодированными Base64 клиентскими именем и паролем в заголовке Authorization, как описано в разделе Получение OAuth токена. В случае успешной аутентификации, страница получает с сервера значение токена доступа, токен записывается в переменную oauthToken, скрывается блок loginForm и отображается блок loggedInStatus.
Чтобы отобразить список покупателей, на сервер отправляется запрос на получение списка экземпляров сущности sales$Customer, передавая значение oauthToken в заголовке Authorization.
Если запрос выполнен, блок customers отображается на экране, и маркированный список customersList заполняется элементами, содержащими значения атрибутов name и email сущности Customer.
4.5.14. Получение локализованных сообщений
REST API позволяет получить локализованные заголовки для сущностей, перечислений и их атрибутов.
Например, чтобы получить локализованные сообщения для сущности sec$User, необходимо выполнить следующий GET запрос:
http://localhost:8080/app/rest/v2/messages/entities/sec$User
OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.
Явно указать локаль запроса можно с помощью http-заголовка Accept-Language.
Ответ будет выглядеть следующим образом:
{
"sec$User": "User",
"sec$User.active": "Active",
"sec$User.changePasswordAtNextLogon": "Change Password at Next Logon",
"sec$User.createTs": "Created At",
"sec$User.createdBy": "Created By",
"sec$User.deleteTs": "Deleted At",
"sec$User.deletedBy": "Deleted By",
"sec$User.email": "Email",
"sec$User.firstName": "First Name",
"sec$User.group": "Group",
"sec$User.id": "ID",
"sec$User.ipMask": "Permitted IP Mask",
"sec$User.language": "Language",
"sec$User.lastName": "Last Name",
"sec$User.login": "Login",
"sec$User.loginLowerCase": "Login",
"sec$User.middleName": "Middle Name",
"sec$User.name": "Name",
"sec$User.password": "Password",
"sec$User.position": "Position",
"sec$User.substitutions": "Substitutions",
"sec$User.timeZone": "Time Zone",
"sec$User.timeZoneAuto": "Autodetect Time Zone",
"sec$User.updateTs": "Updated At",
"sec$User.updatedBy": "Updated By",
"sec$User.userRoles": "User Roles",
"sec$User.version": "Version"
}
Для получения списка локализованных сообщений для перечисления используется следующий запрос:
http://localhost:8080/app/rest/v2/messages/enums/com.haulmont.cuba.security.entity.RoleType
Если из URL убрать часть с именем сущности или перечисления, то будут возвращены локализованные сообщения для всех сущностей или перечислений.
4.5.15. Примеры версионирования модели данных
Атрибут сущности переименован
Предположим, атрибут oldNumber сущности sales$Order был переименован в newNumber, а атрибут date был переименован в deliveryDate. В этом случае конфигурация трансформации будет выглядеть следующим образом:
<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
<transformation modelVersion="1.0" currentEntityName="sales$Order">
<renameAttribute oldName="oldNumber" currentName="newNumber"/>
<renameAttribute oldName="date" currentName="deliveryDate"/>
</transformation>
...
</transformations>
Если клиентскому приложению необходимо работать со старой версии сущности sales$Order, то приложение должно передать значение версии модели данных в параметре URL modelVersion:
http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.0
Будет возвращен следующий результат:
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"date": "2016-10-31",
"description": "Vacation order",
"oldNumber": "00001"
}
Видим, что ответ содержит атрибуты oldNumber и date, хотя сущность в последней версии приложения уже имеет переименованные атрибуты newNumber и deliveryDate.
Имя сущности изменено
Предположим, что в одном из следующих релизов приложения имя сущности sales$Order также было изменено. Новое имя сущности теперь sales$NewOrder.
Конфиг трансформации для версии 1.1 выглядит так:
<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
<transformation modelVersion="1.1" oldEntityName="sales$Order" currentEntityName="sales$NewOrder">
<renameAttribute oldName="oldNumber" currentName="newNumber"/>
</transformation>
...
</transformations>
В дополнение к конфигу из предыдущего примера здесь появился атрибут oldEntityName. Он описывает имя сущности, действительное для версии модели данных 1.1. Атрибут currentEntityName описывает текущее имя сущности.
Хотя сущность с именем sales$Order более не существует, следующий запрос будет работать:
http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1
Контроллер REST API поймет, что поиск должен быть осуществлен среди экземпляров сущности sales$NewOrder, и после того, как сущность с заданным ID будет найдена, имя сущности и имя атрибута newNumber будут заменены в JSON:
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"date": "2016-10-31",
"description": "Vacation order",
"oldNumber": "00001"
}
Клиентское приложение также может использовать старую версию модели данных для создания или изменения сущности.
Этот POST запрос использует старое имя сущности и имеет JSON в старом формате в теле запроса, однако, запрос будет работать:
http://localhost:8080/app/rest/v2/entities/sales$Order
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"date": "2016-10-31",
"description": "Vacation order",
"oldNumber": "00001"
}
Атрибут должен быть удален из JSON
Иногда может возникнуть ситуация, когда в сущность был добавлен новый атрибут, но клиент, работающий со старой версией модели данных, не должен получать этот атрибут при запросе к сущности. В этом случае стандартный JSON трансформер может удалить атрибут из результата.
Конфиг трансформации для данного случае выглядит примерно так:
<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
<transformation modelVersion="1.5" currentEntityName="sales$Order">
<toVersion>
<removeAttribute name="discount"/>
</toVersion>
</transformation>
...
</transformations>
Описание трансформации здесь содержит тег toVersion с вложенной командой removeAttribute. Это значит, что при выполнении трансформации из текущей версии модели данных к определенной версии (например, при запросе списка сущностей) атрибут discount будет удален из JSON.
Если выполнить запрос к REST API без атрибута modelVersion, то атрибут discount будет возвращен.
http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"deliveryDate": "2016-10-31",
"description": "Vacation order",
"number": "00001",
"discount": 50
}
Если указать modelVersion в запросе, то атрибут discount будет удален:
http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"deliveryDate": "2016-10-31",
"description": "Vacation order",
"oldNumber": "00001"
}
Использование кастомных трансформеров
Вы также можете создать и зарегистрировать свой собственный трансформер JSON. В качестве примера рассмотрим следующую ситуацию.
Сущность sales$OldOrder была переименована в sales$NewOrder. В сущности имеется поле orderDate с типом дата. В старой версии сущности это поле содержало часть со временем, в новой версии сущности часть со временем в поле отсутствует. Клиент REST API, запрашивающий сущность со старой версией модели данных 1.0 ожидает, что дата будет иметь часть со временем. Получается, что JSON трансформер должен изменить значение в JSON.
Так будет выглядеть конфигурация трансформации для данного случая:
<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
<transformation modelVersion="1.0" oldEntityName="sales$OldOrder" currentEntityName="sales$NewOrder">
<custom>
<fromVersion transformerBeanRef="sales_OrderJsonTransformerFromVersion"/>
<toVersion transformerBeanRef="sales_OrderJsonTransformerToVersion"/>
</custom>
</transformation>
...
</transformations>
Конфигурация содержит элемент custom с вложенными элементами toVersion и fromVersion. Эти элементы содержат ссылки на бины, т.е. кастомный трансформер должен быть зарегистрирован как Spring bean.
Важная деталь: в кастомном трансформере возможно потребуется использовать бин платформы RestTransformations (он предоставляет доступ к трансформерам для других сущностей). Но бин RestTransformations зарегистрирован в Spring контексте сервлета REST API, а не в главном контексте веб-приложения. Это значит, что бин кастомного трансформера также должен быть зарегистрирован в контексте REST API.
Как это сделать. Во-первых, создайте файл rest-dispatcher-spring.xml в модуле web или portal (например в пакете com.company.test).
Затем зарегистрируйте этот файл в файле свойств app.properties соответствующего модуля.
cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml
Файл rest-dispatcher-spring.xml должен содержать определения бинов для кастомных трансформеров:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<bean name="sales_OrderJsonTransformerFromVersion" class="com.company.test.transformer.OrderJsonTransformerFromVersion"/>
<bean name="sales_OrderJsonTransformerToVersion" class="com.company.test.transformer.OrderJsonTransformerToVersion"/>
</beans>
Исходный код бина sales_OrderJsonTransformerToVersion:
package com.company.test.transformer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.haulmont.restapi.transform.AbstractEntityJsonTransformer;
import com.haulmont.restapi.transform.JsonTransformationDirection;
public class OrderJsonTransformerToVersion extends AbstractEntityJsonTransformer {
public OrderJsonTransformerToVersion() {
super("sales$NewOrder", "sales$OldOrder", "1.0", JsonTransformationDirection.TO_VERSION);
}
@Override
protected void doCustomTransformations(ObjectNode rootObjectNode, ObjectMapper objectMapper) {
JsonNode orderDateNode = rootObjectNode.get("orderDate");
if (orderDateNode != null) {
String orderDateNodeValue = orderDateNode.asText();
if (!Strings.isNullOrEmpty(orderDateNodeValue))
rootObjectNode.put("orderDate", orderDateNodeValue + " 00:00:00.000");
}
}
}
Данный трансформер находит элемент orderDate в JSON и изменяет значение элемента, добавляя к нему часть со временем.
Когда сущность sales$Order будет запрошена с версией модели данных 1.0, то JSON результат будет содержать сущность, поле orderDate которой содержит часть со временем, хотя текущий тип поля сущности - дата без времени.
Несколько слов о кастомных трансформерах. Они должны реализовывать интерфейс EntityJsonTransformer. Вы также можете унаследоваться от класса AbstractEntityJsonTransformer и переопределить его метод doCustomTransformations. Класс AbstractEntityJsonTransformer содержит функциональность стандартных трансформеров.
4.5.16. Поиск сущностей с фильтром
REST API даёт возможность искать экземпляры сущностей по определенным условиям.
Примеры мы рассмотрим на модели данных, состоящей из двух сущностей:
Чтобы выполнить поиск с фильтром URL должен выглядеть следующим образом:
http://localhost:8080/app/rest/v2/entities/test$Book/search
Условия поиска должны быть переданы в параметре filter. filter - это JSON объект, который содержит набор условий поиска. Если поиск выполняется с помощью GET запроса, то параметр filter передается в URL.
Пример 1
Необходимо найти все книги, опубликованные в 2007 году с именем автора, начинающимся с "Alex". JSON фильтр для подобного условия будет выглядеть так:
{
"conditions": [
{
"property": "author.firstName",
"operator": "startsWith",
"value": "Alex"
},
{
"property": "publicationDate",
"operator": "=",
"value": 2007
}
]
}
По умолчанию все критерии поиска применяются с условием И.
Данный пример также демонстрирует возможность использования вложенных свойств объекта (author.firstName).
Пример 2
Следующий пример показывает две вещи: как выполнять поиск с помощью POST запроса и как использовать группы ИЛИ. В случае POST все параметры должны быть переданы в JSON объекте в теле запроса. Условия поиска должны быть помещены в поле объекта с именем filter. Остальные параметры (имя view, количество выгружаемых сущностей и т.п.) должны быть помещены в поля объекта с соответствующими именами:
{
"filter": {
"conditions": [
{
"group": "OR",
"conditions": [
{
"property": "author.lastName",
"operator": "contains",
"value": "Stev"
},
{
"property": "author.lastName",
"operator": "=",
"value": "Dumas"
}
]
},
{
"property": "publicationDate",
"operator": "=",
"in": [2007, 2008]
}
]
},
"view": "book-view"
}
В этом примере коллекция conditions содержит не только объекты с критериями поиска, но и группу ИЛИ (OR). Итоговое условие можно представить так:
((author.lastName contains Stev) OR (author.lastName = Duma) AND (publicationDate in [2007, 2008]))
Обратите внимание, что параметр view также передан в теле запроса.
4.6. Работа с компонентами приложений
Любое CUBA-приложение может быть компонентом другого приложения. Компонент приложения представляет собой по сути full-stack библиотеку, предоставляющую функциональность на всех уровнях - от схемы БД до бизнес-логики и UI.
Компоненты, опубликованные в маркетплейсе CUBA в качестве аддонов, расширяют функциональность фреймворка и могут быть использованы в любом приложении, созданном на платформе.
4.6.1. Использование публичных аддонов
Аддоны, доступные на маркетплейсе CUBA, можно добавить к приложению одним из способов, описанных ниже. Первый и второй способы подразумевают, что в приложении настроен доступ к одному из стандартных репозиториев CUBA. Третий подход применим к аддонам с открытым исходным кодом и не требует подключения к какому-либо удалённому репозиторию.
com.haulmont.addon.cubajm:cuba-jm-global:0.3.1
dependencies {
appComponent("com.haulmont.cuba:cuba-global:$cubaVersion")
// your add-ons go here
appComponent("com.haulmont.addon.cubajm:cuba-jm-global:0.3.1")
}
<context-param>
<param-name>appComponents</param-name>
<param-value>com.haulmont.cuba com.haulmont.addon.cubajm</param-value>
</context-param>
4.6.2. Создание компонентов приложения
В этом разделе описаны рекомендации по созданию компонентов приложения с целью повторного использования.
Имя корневого пакета не должно начинаться с имени любого другого компонента или приложения. К примеру, если корневой пакет вашего приложения com.jupiter.tickets, вы НЕ можете использовать пакет com.jupiter.tickets.amazingsearch для компонента. Это обусловлено тем, что Spring сканирует classpath бинов, начиная с указанного корневого пакета, и это сканирование должно быть уникальным для всех компонентов.
@Component("jptams_Finder")
@Property("jptams.ignoreCase")
Установка в локальный Maven-репозиторий
Чтобы сделать компонент доступным для использования в проектах, расположенных на том же компьютере, установите его в локальный репозиторий Maven, выполнив команду Run > Install app component в меню Studio. Данная команда просто запускает задачу Gradle install после остановки демонов Gradle.
Загрузка в удалённый Maven-репозиторий
cuba {
//...
// repository for uploading your artifacts
uploadRepository {
url = 'http://repo.company.com/nexus/content/repositories/snapshots'
user = 'admin'
password = 'admin123'
}
}
|
Для входа на Bintray можно использовать social login (через GitHub, Gmail или Twitter), однако позже вам потребуется выполнить сброс пароля, так как для получения API-ключа (см.ниже) понадобится пароль от учетной записи. |
BINTRAY_USER=your_bintray_user
BINTRAY_API_KEY=9696c1cb90752357ded8fdf20eb3fa921bf9dbbb
bintray {
user = 'bintray_user'
key = 'bintray_api_key'
...
}
./gradlew clean assemble bintrayUpload -Pcuba.artifact.version=1.0.0 -PbintrayUser=your_bintray_user -PbintrayApiKey=9696c1cb90752357ded8fdf20eb3fa921bf9dbbb
Структура Bintray предполагает использование пакетов (packages) внутри репозиториев. На данном этапе создавать пакет необязательно, так как он будет создан автоматически в ходе задания gradle bintrayUpload.
buildscript {
// ...
dependencies {
classpath "com.haulmont.gradle:cuba-plugin:$cubaVersion"
// Bintray upload plugin
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0"
}
}
/** * If you have a multi-project build, make sure to apply the plugin and the plugin configuration to every project which artifacts you want to publish to Bintray. */
subprojects {
apply plugin: 'com.jfrog.bintray'
bintray {
user = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER')
key = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY')
configurations = ['archives']
// make files public ?
publish = true
// override existing artifacts?
override = false
// metadata
pkg {
repo = 'main' // your repository name
name = 'amazingsearch' // package name - it will be created upon upload
desc = 'AmasingSearch' // optional package description
// organization name, if your repository is created inside an organization.
// remove this parameter if you don't have an organization
userOrg = 'jupiter-org'
websiteUrl = 'https://github.com/jupiter/amazing-search'
issueTrackerUrl = 'https://github.com/jupiter/amazing-search/issues'
vcsUrl = 'https://github.com/jupiter/amazing-search.git' // mandatory for Open Source projects
licenses = ["Apache-2.0"]
labels = ['cuba-platform', 'opensource']
//githubRepo = 'amazingsearch/cuba-platform' // optional Github repository
//githubReleaseNotesFile = 'README.md' // optional Github readme file
}
}
}
./gradlew clean assemble bintrayUpload -Pcuba.artifact.version=1.0.0
4.6.3. Пример создания и использования компонента
В данном разделе рассматривается пример создания компонента приложения и использования его в проекте. Компонент будет предоставлять функциональность "Customer Management" и содержать сущность Customer и соответствующие экраны UI. Приложение будет использовать сущность Customer из компонента в качестве ссылки в собственной сущности Order.
Создание компонента Customer Management
|
Если компонент содержит персистентные классы, аннотированные @MappedSuperclass, убедитесь, что в этом же проекте есть их наследники, являющиеся сущностями (т.е. аннотированные @Entity). В противном случае байткод таких базовых классов не будет необходимым образом модифицирован и они не будут правильно работать в приложениях, использующих компонент. |
Создание приложения Sales
Модификация компонента Customer Management
Предположим, что необходимо изменить функциональность компонента (добавить атрибут в сущность Customer) и пересобрать приложение для внесения этих изменений.
4.6.4. Регистрация DispatcherServlet из компонента приложения
В этом разделе мы рассмотрим, как настроить динамическую регистрацию сервлетов и фильтров, настроенных в компоненте приложения, в родительском приложении. Чтобы избежать дублирования кода в файле конфигурации web.xml, необходимо зарегистрировать сервлеты и фильтры с помощью специального бина ServletRegistrationManager.
Простой пример такой регистрации рассмотрен на примере HTTP-сервлета. Здесь мы рассмотрим более сложный случай: частную реализацию сервлета DispatcherServlet в компоненте приложения. Этот сервлет будет загружать параметры своей конфигурации из файла demo-dispatcher-spring.xml, поэтому, чтобы попробовать этот пример на практике, вы должны создать пустой файл с этим именем в корневом каталоге ресурсов проекта (например, web/src).
public class WebDispatcherServlet extends DispatcherServlet {
private volatile boolean initialized = false;
@Override
public String getContextConfigLocation() {
String configFile = "demo-dispatcher-spring.xml";
File baseDir = new File(AppContext.getProperty("cuba.confDir"));
String[] tokenArray = new StrTokenizer(configFile).getTokenArray();
StringBuilder locations = new StringBuilder();
for (String token : tokenArray) {
String location;
if (ResourceUtils.isUrl(token)) {
location = token;
} else {
if (token.startsWith("/"))
token = token.substring(1);
File file = new File(baseDir, token);
if (file.exists()) {
location = file.toURI().toString();
} else {
location = "classpath:" + token;
}
}
locations.append(location).append(" ");
}
return locations.toString();
}
@Override
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
ApplicationContext parent = AppContext.getApplicationContext();
wac = createWebApplicationContext(parent);
}
onRefresh(wac);
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
return wac;
}
@Override
public void init(ServletConfig config) throws ServletException {
if (!initialized) {
super.init(config);
initialized = true;
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
_service(response);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
_service(res);
}
private void _service(ServletResponse res) throws IOException {
String testMessage = AppContext.getApplicationContext().getBean(Messages.class).getMainMessage("testMessage");
res.getWriter()
.write("WebDispatcherServlet test message: " + testMessage);
}
}
Чтобы зарегистрировать DispatcherServlet, вам нужно вручную загрузить класс, создать его экземпляр и проинициализировать его, в противном случае использование разных типов ClassLoader может вызвать проблемы при развёртывании в SingleWAR/SingleUberJAR. Кроме того, собственная реализация DispatcherServlet должна выдерживать двойную инициализацию - сначала вручную, а затем с помощью servlet container.
Пример компонента для инициализации WebDispatcherServlet:
@Component
public class WebInitializer {
private static final String WEB_DISPATCHER_CLASS = "com.demo.comp.web.WebDispatcherServlet";
private static final String WEB_DISPATCHER_NAME = "web_dispatcher_servlet";
private final Logger log = LoggerFactory.getLogger(WebInitializer.class);
@Inject
private ServletRegistrationManager servletRegistrationManager;
@EventListener
public void initialize(ServletContextInitializedEvent e) {
Servlet webDispatcherServlet = servletRegistrationManager.createServlet(e.getApplicationContext(), WEB_DISPATCHER_CLASS);
ServletContext servletContext = e.getSource();
try {
webDispatcherServlet.init(new AbstractWebAppContextLoader.CubaServletConfig(WEB_DISPATCHER_NAME, servletContext));
} catch (ServletException ex) {
throw new RuntimeException("Failed to init WebDispatcherServlet");
}
servletContext.addServlet(WEB_DISPATCHER_NAME, webDispatcherServlet)
.addMapping("/webd/*");
}
}
Метод createServlet() инжектированного бина ServletRegistrationManager принимает контекст приложения, полученный из события ServletContextInitializedEvent, и полное имя класса WebDispatcherServlet. Для инициализации сервлета мы передаём экземпляр ServletContext, также полученный из события ServletContextInitializedEvent, и имя сервлета. В методе addMapping() задаём HTTP-мэппинг для отображения на URL: /webd/.
В данном разделе собраны рецепты, которые сложно отнести к одной из категорий выше.
4.7.1. Получение локализованных сообщений
В данном разделе рассмотрены способы получения локализованных сообщений в различных компонентах приложения.
String msg = getMessage("warningMessage");
messages.properties:
warningMessage = Invalid email address: '%s'
Java-контроллер:
String msg = formatMessage("warningMessage", email);
@Inject
private Messages messages;
@Override
public void init(Map<String, Object> params) {
String msg = messages.getMessage(getClass(), "warningMessage");
...
}
@Inject
protected Messages messages;
...
String msg = messages.getMessage(getClass(), "warningMessage");
Локализованные сообщения для шаблонов Thymeleaf в модуле portal также доступны по ключу:
template
<h1 th:text="#{messageKey}"></h1>
portal main message pack
messageKey = Localized message
protected Messages messages = AppBeans.get(Messages.class);
...
String msg = messages.getMessage(getClass(), "warningMessage");
4.7.2. Загрузка и вывод изображений
Рассмотрим задачу загрузки, хранения и отображения фотографий сотрудников:
Класс сущности со ссылкой на файл изображения:
@Table(name = "SAMPLE_EMPLOYEE")
@Entity(name = "sample$Employee")
public class Employee extends StandardEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "IMAGE_FILE_ID")
protected FileDescriptor imageFile;
public void setImageFile(FileDescriptor imageFile) {
this.imageFile = imageFile;
}
public FileDescriptor getImageFile() {
return imageFile;
}
}
Представление для загрузки Employee вместе с FileDescriptor должно содержать все локальные атрибуты FileDescriptor:
<view class="com.company.sample.entity.Employee"
name="employee-edit">
<property name="name"/>
...
<property name="imageFile"
view="_local">
</property>
</view>
Фрагмент XML-дескриптора экрана редактирования Employee:
<groupBox caption="Photo" spacing="true"
height="300px" width="300px" expand="image">
<image id="image"
width="100%"
align="MIDDLE_CENTER"
scaleMode="CONTAIN"/>
<hbox align="BOTTOM_LEFT"
spacing="true">
<upload id="uploadField"/>
<button id="downloadImageBtn"
caption="Download"
invoke="onDownloadImageBtnClick"/>
<button id="clearImageBtn"
caption="Clear"
invoke="onClearImageBtnClick"/>
</hbox>
</groupBox>
Компоненты отображения и загрузки/выгрузки фотографии заключены внутрь контейнера groupBox. В верхней его части с помощью компонента image выводится изображение, а в нижней слева направо расположены компонент upload для загрузки файла и кнопки выгрузки и очистки изображения. В результате эта часть экрана должна выглядеть следующим образом:
Теперь рассмотрим контроллер экрана редактирования.
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.core.global.FileStorageException;
import com.haulmont.cuba.gui.components.*;
import com.company.employeeimages.entity.Employee;
import com.haulmont.cuba.gui.data.DataSupplier;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.export.ExportDisplay;
import com.haulmont.cuba.gui.export.ExportFormat;
import com.haulmont.cuba.gui.upload.FileUploadingAPI;
import javax.inject.Inject;
import java.util.Map;
public class EmployeeEdit extends AbstractEditor<Employee> {
@Inject
private DataSupplier dataSupplier;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private ExportDisplay exportDisplay;
@Inject
private FileUploadField uploadField;
@Inject
private Button downloadImageBtn;
@Inject
private Button clearImageBtn;
@Inject
private Datasource<Employee> employeeDs;
@Inject
private Image image;
@Override
public void init(Map<String, Object> params) {
uploadField.addFileUploadSucceedListener(event -> {
FileDescriptor fd = uploadField.getFileDescriptor();
try {
fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error saving file to FileStorage", e);
}
getItem().setImageFile(dataSupplier.commit(fd));
displayImage();
});
uploadField.addFileUploadErrorListener(event ->
showNotification("File upload error", NotificationType.HUMANIZED));
employeeDs.addItemPropertyChangeListener(event -> {
if ("imageFile".equals(event.getProperty()))
updateImageButtons(event.getValue() != null);
});
}
@Override
protected void postInit() {
displayImage();
updateImageButtons(getItem().getImageFile() != null);
}
public void onDownloadImageBtnClick() {
if (getItem().getImageFile() != null)
exportDisplay.show(getItem().getImageFile(), ExportFormat.OCTET_STREAM);
}
public void onClearImageBtnClick() {
getItem().setImageFile(null);
displayImage();
}
private void updateImageButtons(boolean enable) {
downloadImageBtn.setEnabled(enable);
clearImageBtn.setEnabled(enable);
}
private void displayImage() {
if (getItem().getImageFile() != null) {
image.setSource(FileDescriptorResource.class).setFileDescriptor(getItem().getImageFile());
image.setVisible(true);
} else {
image.setVisible(false);
}
}
}
Далее в методе init() источнику данных, содержащему редактируемый экземпляр Employee, добавляется слушатель для запрещения или разрешения кнопок выгрузки и очистки файла в зависимости от того, загружен файл или нет.
4.7.2.1. Вывод изображений в колонках таблицы
Расширим задачу из предыдущего примера, настроив отображение фотографий на экране просмотра списка сотрудников.
Изображения можно отобразить как в отдельной колонке, так и внутри существующих колонок. В обоих случаях будет использоваться интерфейс Table.ColumnGenerator.
Фрагмент XML-дескриптора экрана просмотра списка Employee:
<groupTable id="employeesTable"
width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
</actions>
<columns>
<column id="name"/>
</columns>
<rows datasource="employeesDs"/>
<rowsCount/>
<buttonsPanel id="buttonsPanel"
alwaysVisible="true">
<button id="createBtn"
action="employeesTable.create"/>
<button id="editBtn"
action="employeesTable.edit"/>
<button id="removeBtn"
action="employeesTable.remove"/>
</buttonsPanel>
</groupTable>
Чтобы отображать фотографию рядом с именем сотрудника в колонке name, необходимо изменить стандартное представление данных в этой колонке. Например, можно использовать контейнер HBoxLayout, поместив внутрь него компонент Image:
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.gui.components.*;
import com.company.employeeimages.entity.Employee;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import javax.inject.Inject;
import java.util.Map;
import static com.haulmont.cuba.gui.components.Image.*;
public class EmployeeBrowse extends AbstractLookup {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private GroupTable<Employee> employeesTable;
@Override
public void init(Map<String, Object> params) {
employeesTable.addGeneratedColumn("name", entity -> {
Image image = componentsFactory.createComponent(Image.class);
image.setScaleMode(ScaleMode.CONTAIN);
image.setHeight("40");
image.setWidth("40");
FileDescriptor userImageFile = entity.getImageFile();
image.setSource(FileDescriptorResource.class).setFileDescriptor(userImageFile);
Label userLogin = componentsFactory.createComponent(Label.class);
userLogin.setValue(entity.getName());
userLogin.setAlignment(Alignment.MIDDLE_LEFT);
HBoxLayout hBox = componentsFactory.createComponent(HBoxLayout.class);
hBox.setSpacing(true);
hBox.add(image);
hBox.add(userLogin);
return hBox;
});
}
}
Можно использовать также более декларативный подход с атрибутом generator.
В данном разделе рассматривается пример использования механизма рассылки email.
Рассмотрим следующую задачу:
public class NewsItemEdit extends AbstractEditor<NewsItem> {
// Indicates that a new item was created in this editor
private boolean justCreated;
@Inject
protected EmailService emailService;
// This method is invoked when a new item is initialized
@Override
protected void initNewItem(NewsItem item) {
justCreated = true;
}
// This method is invoked after the screen commit
@Override
protected boolean postCommit(boolean committed, boolean close) {
if (committed && justCreated) {
// If a new entity was saved to the database, ask a user about sending an email
showOptionDialog(
"Email",
"Send the news item by email?",
MessageType.CONFIRMATION,
new Action[] {
new DialogAction(DialogAction.Type.YES) {
@Override
public void actionPerform(Component component) {
sendByEmail();
}
},
new DialogAction(DialogAction.Type.NO)
}
);
}
return super.postCommit(committed, close);
}
// Queues an email for sending asynchronously
private void sendByEmail() {
NewsItem newsItem = getItem();
EmailInfo emailInfo = new EmailInfo(
"john.doe@company.com,jane.roe@company.com", // recipients
newsItem.getCaption(), // subject
null, // the "from" address will be taken from the "cuba.email.fromAddress" app property
"com/company/demo/templates/news_item.txt", // body template
Collections.singletonMap("newsItem", newsItem) // template parameters
);
emailService.sendEmailAsync(emailInfo);
}
}
Как видно, метод sendByEmail() вызывает сервис EmailService и передает ему экземпляр EmailInfo, описывающий сообщение. Тело сообщений будет создаваться на основе шаблона news_item.txt.
The company news:
${newsItem.content}
Это шаблон Freemarker, который использует параметры, переданные в EmailInfo (в данном случае единственный параметр newsItem).
Сохраните задачу и нажмите на ней Activate.
Если вы ранее не настраивали выполнение назначенных заданий для данного приложения ранее, то на данном этапе ничего не произойдет - новая задача не начнет выполняться, пока вы не запустите весь механизм назначенных заданий.
cuba.schedulingActive = true
Перезапустите сервер приложения. Механизм выполнения заданий теперь активен и вызывает обработку очереди email.
cuba.email.fromAddress = do-not-reply@company.com
cuba.email.smtpHost = mail.company.com
Перезапустите сервер приложения. Перейдите в экран Administration > JMX Console, найдите JMX бин Emailer и попробуйте послать самому себе тестовое сообщение с помощью операции sendTestEmail().
4.7.4. Создание собственных визуальных компонентов
В разделе Собственные визуальные компоненты был приведен обзор методов расширения набора стандартных визуальных компонентов в проекте. У вас есть следующие варианты:
Далее вы можете интегрировать получившийся компонент Vaadin в универсальный пользовательский интерфейс CUBA, чтобы иметь возможность использовать его декларативно в XML-дескрипторах экранов и привязывать к источникам данных.
Финальным шагом интеграции является поддержка нового компонента в WYSIWYG редакторе экранов Studio.
Далее в этом разделе приводятся примеры создания новых визуальных компонентов каждым из описанных выше способов. Интеграция в Generic UI одинакова для всех трех способов, поэтому она описана только для примера с подключением аддона Vaadin.
4.7.4.1. Подключение аддона Vaadin
Рассмотрим пример использования компонента Stepper, доступного по адресу http://vaadin.com/addon/stepper. Данный компонент позволяет пошагово изменять значение текстового поля с помощью клавиатуры, колесика мыши и встроенных кнопок вверх/вниз.
Создайте новый проект в CUBA Studio и назовите его addon-demo.
Для подключения аддона Vaadin проект должен иметь модуль web-toolkit. Создайте его, нажав на ссылку Create web toolkit module секции Project properties навигатора.
Далее нажмите на ссылку New UI component. Откроется окно создания визуального компонента UI component generation. В секции Component type выберите значение Vaadin add-on.
Заполните следующие поля:
<dependency>
<groupId>org.vaadin.addons</groupId>
<artifactId>stepper</artifactId>
<version>2.2.2</version>
</dependency>
org.vaadin.risto.stepper.widgetset.StepperWidgetset
Нажмите кнопку OK.
Если открыть проект в IDE, то можно увидеть, что Студия изменила два файла:
configure(webModule) {
...
dependencies {
...
compile("org.vaadin.addons:stepper:2.2.2")
}
<module>
<inherits name="com.haulmont.cuba.web.toolkit.ui.WidgetSet" />
<inherits name="org.vaadin.risto.stepper.widgetset.StepperWidgetset" />
<set-property name="user.agent" value="safari" />
</module>
|
Для более быстрой сборки виджетов на время разработки вы можете установить свойство user.agent. В данном примере набор виджетов будет собираться только для браузеров, основанных на WebKit: Chrome, Safari, и т.д. |
Компонент из аддона Vaadin подключен. Далее мы покажем как использовать его в экранах проекта.
|
На самом деле экран может располагаться и в модуле gui, но тогда код, работающий с Vaadin компонентом, должен быть вынесен в отдельный компаньон. |
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.addondemo.web.customer.CustomerEdit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addondemo.web.customer">
<dsContext>
<datasource id="customerDs"
class="com.company.addondemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions" spacing="true">
<fieldGroup id="fieldGroup" datasource="customerDs">
<column width="250px">
<field property="name"/>
<field property="score" custom="true"/>
</column>
</fieldGroup>
<frame id="windowActions" screen="editWindowActions"/>
</layout>
</window>
В контроллер экрана редактирования CustomerEdit.java добавим следующий код:
package com.company.addondemo.web.customer;
import com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.addondemo.entity.Customer;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.FieldGroup;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import com.vaadin.ui.Layout;
import org.vaadin.risto.stepper.IntStepper;
import javax.inject.Inject;
import java.util.Map;
public class CustomerEdit extends AbstractEditor<Customer> {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private FieldGroup fieldGroup;
@Inject
private Datasource<Customer> customerDs;
private IntStepper stepper = new IntStepper();
@Override
public void init(Map<String, Object> params) {
fieldGroup.createField("score");
Component box = componentsFactory.createComponent(VBoxLayout.class);
fieldGroup.getFieldNN("score").setComponent(box);
Layout layout = (Layout) WebComponentsHelper.unwrap(box);
layout.addComponent(stepper);
stepper.setSizeFull();
stepper.addValueChangeListener(event ->
customerDs.getItem().setValue("score", event.getProperty().getValue())
);
}
@Override
protected void initNewItem(Customer item) {
item.setScore(0);
}
@Override
protected void postInit() {
stepper.setValue(getItem().getScore());
}
}
Здесь в поле stepper создается экземпляр компонента, подключенного из аддона. В методе init() производится инициализация кастомного поля score. Через ComponentsFactory создается экземпляр BoxLayout, затем из него с помощью WebComponentsHelper извлекается ссылка на Vaadin-контейнер, и в этот контейнер добавляется наш новый компонент. BoxLayout возвращается для отображения в кастомном поле.
Для связи компонента с данными, во-первых, в методе postInit() ему устанавливается текущее значение из редактируемого Customer, а во-вторых, добавляется слушатель на изменение значения, который обновляет соответствующий атрибут сущности при изменении значения пользователем.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.addondemo.web.customer.CustomerEdit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addondemo.web.customer">
<dsContext>
<datasource id="customerDs"
class="com.company.addondemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions" spacing="true">
<fieldGroup id="fieldGroup" datasource="customerDs">
<column width="250px">
<field property="name"/>
</column>
</fieldGroup>
<hbox id="scoreBox" spacing="true">
<label value="Score" align="MIDDLE_LEFT"/>
</hbox>
<frame id="windowActions" screen="editWindowActions"/>
</layout>
</window>
В контроллере инжектируем контейнер, извлекаем ссылку на Vaadin-контейнер и добавляем в него компонент:
package com.company.addondemo.web.customer;
import com.haulmont.cuba.gui.components.*;
import com.company.addondemo.entity.Customer;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import com.vaadin.ui.Layout;
import org.vaadin.risto.stepper.IntStepper;
import javax.inject.Inject;
import java.util.Map;
public class CustomerEdit extends AbstractEditor<Customer> {
@Inject
private FieldGroup fieldGroup;
@Inject
private Datasource<Customer> customerDs;
@Inject
private BoxLayout scoreBox;
private IntStepper stepper = new IntStepper();
@Override
public void init(Map<String, Object> params) {
Layout box = (Layout) WebComponentsHelper.unwrap(scoreBox);
box.addComponent(stepper);
stepper.setWidth("250px");
fieldGroup.addField(fieldGroup.createField("score"));
stepper.addValueChangeListener(event ->
customerDs.getItem().setValue("score", event.getProperty().getValue())
);
}
@Override
protected void initNewItem(Customer item) {
item.setScore(0);
}
@Override
protected void postInit() {
stepper.setValue(getItem().getScore());
}
}
Связь с данными выполняется здесь аналогично примеру с FieldGroup.
/* Define your theme modifications inside next mixin */
@mixin com_company_application-halo-ext {
@include halo;
/* Basic styles for stepper inner text box */
.stepper input[type="text"] {
@include box-defaults;
@include valo-textfield-style;
&:focus {
@include valo-textfield-focus-style;
}
}
}
4.7.4.2. Подключение аддона Vaadin с интеграцией в Generic UI
В предыдущем разделе мы подключили в проект сторонний компонент Stepper. В данном разделе мы интегрируем его в универсальный пользовательский интерфейс CUBA, что даст разработчикам возможность использовать компонент декларативно в XML-дескрипторах экранов и связывать его с сущностями через источники данных.
Создадим новый проект в CUBA Studio и назовем его addon-gui-demo.
Создадим модуль web-toolkit, нажав на кнопку Create web toolkit module секции Project properties навигатора Studio.
Далее нажимаем на кнопку New UI component. Откроется окно создания визуального компонента UI component generation. В секции Component type выбираем значение Vaadin add-on.
Заполним поля Add-on Maven dependency и Inherited widgetset как описано в предыдущем разделе.
Далее заполним поля в нижней секции:
После нажатия кнопки OK Studio сделает следующее:
Откройте проект в IDE.
Последовательно пройдемся по сгенерированным Studio заготовкам файлов и внесем в них необходимые изменения.
package com.company.addonguidemo.web.gui.components;
import com.haulmont.cuba.gui.components.Field;
// note that Stepper should extend Field
public interface Stepper extends Field {
String NAME = "stepper";
boolean isManualInputAllowed();
void setManualInputAllowed(boolean value);
boolean isMouseWheelEnabled();
void setMouseWheelEnabled(boolean value);
int getStepAmount();
void setStepAmount(int amount);
int getMaxValue();
void setMaxValue(int maxValue);
int getMinValue();
void setMinValue(int minValue);
}
В качестве базового для нашего компонента выбран интерфейс Field. Это позволяет осуществить связь с данными (data binding), то есть отображать и редактировать значение некоторого атрибута сущности.
package com.company.addonguidemo.web.gui.components;
import com.company.addonguidemo.web.gui.components.Stepper;
import com.haulmont.cuba.web.gui.components.WebAbstractField;
import org.vaadin.risto.stepper.IntStepper;
// note that WebStepper should extend WebAbstractField
public class WebStepper extends WebAbstractField<IntStepper> implements Stepper {
public WebStepper() {
this.component = new org.vaadin.risto.stepper.IntStepper();
}
@Override
public boolean isManualInputAllowed() {
return component.isManualInputAllowed();
}
@Override
public void setManualInputAllowed(boolean value) {
component.setManualInputAllowed(value);
}
@Override
public boolean isMouseWheelEnabled() {
return component.isMouseWheelEnabled();
}
@Override
public void setMouseWheelEnabled(boolean value) {
component.setMouseWheelEnabled(value);
}
@Override
public int getStepAmount() {
return component.getStepAmount();
}
@Override
public void setStepAmount(int amount) {
component.setStepAmount(amount);
}
@Override
public int getMaxValue() {
return component.getMaxValue();
}
@Override
public void setMaxValue(int maxValue) {
component.setMaxValue(maxValue);
}
@Override
public int getMinValue() {
return component.getMinValue();
}
@Override
public void setMinValue(int minValue) {
component.setMinValue(minValue);
}
}
В качестве базового класса выбран WebAbstractField, который реализует логику интерфейса Field.
package com.company.addonguidemo.web.gui.xml.layout.loaders;
import com.company.addonguidemo.web.gui.components.Stepper;
import com.haulmont.cuba.gui.xml.layout.loaders.AbstractFieldLoader;
public class StepperLoader extends AbstractFieldLoader<Stepper> {
@Override
public void createComponent() {
resultComponent = factory.createComponent(Stepper.class);
loadId(resultComponent, element);
}
@Override
public void loadComponent() {
super.loadComponent();
String manualInput = element.attributeValue("manualInput");
if (manualInput != null) {
resultComponent.setManualInputAllowed(Boolean.parseBoolean(manualInput));
}
String mouseWheel = element.attributeValue("mouseWheel");
if (mouseWheel != null) {
resultComponent.setMouseWheelEnabled(Boolean.parseBoolean(mouseWheel));
}
String stepAmount = element.attributeValue("stepAmount");
if (stepAmount != null) {
resultComponent.setStepAmount(Integer.parseInt(stepAmount));
}
String maxValue = element.attributeValue("maxValue");
if (maxValue != null) {
resultComponent.setMaxValue(Integer.parseInt(maxValue));
}
String minValue = element.attributeValue("minValue");
if (minValue != null) {
resultComponent.setMinValue(Integer.parseInt(minValue));
}
}
}
Логика загрузки базовых свойств компонента Field сосредоточена в классе AbstractFieldLoader. Нам достаточно загрузить только специфические свойства Stepper.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<components xmlns="http://schemas.haulmont.com/cuba/components.xsd">
<component>
<name>stepper</name>
<componentLoader>com.company.addonguidemo.web.gui.xml.layout.loaders.StepperLoader</componentLoader>
<class>com.company.addonguidemo.web.gui.components.WebStepper</class>
</component>
</components>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xs:schema xmlns="http://schemas.company.com/agd/0.1/ui-component.xsd"
attributeFormDefault="unqualified"
elementFormDefault="qualified"
targetNamespace="http://schemas.company.com/agd/0.1/ui-component.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="stepper">
<xs:complexType>
<xs:attribute name="id" type="xs:string"/>
<xs:attribute name="caption" type="xs:string"/>
<xs:attribute name="width" type="xs:string"/>
<xs:attribute name="height" type="xs:string"/>
<xs:attribute name="datasource" type="xs:string"/>
<xs:attribute name="property" type="xs:string"/>
<xs:attribute name="manualInput" type="xs:boolean"/>
<xs:attribute name="mouseWheel" type="xs:boolean"/>
<xs:attribute name="stepAmount" type="xs:int"/>
<xs:attribute name="maxValue" type="xs:int"/>
<xs:attribute name="minValue" type="xs:int"/>
</xs:complexType>
</xs:element>
</xs:schema>
Далее рассмотрим, как добавить новый компонент на экран.
В результате XML-дескриптор редактора должен выглядеть так:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd"
caption="msg://editCaption"
class="com.company.addonguidemo.web.customer.CustomerEdit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addonguidemo.web.customer">
<dsContext>
<datasource id="customerDs"
class="com.company.addonguidemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions" spacing="true">
<fieldGroup id="fieldGroup" datasource="customerDs">
<column width="250px">
<field property="name"/>
</column>
</fieldGroup>
<app:stepper id="stepper" datasource="customerDs" property="score" caption="Score"
minValue="1" maxValue="20"/>
<frame id="windowActions" screen="editWindowActions"/>
</layout>
</window>
В данном примере компонент stepper подсоединен к атрибуту score сущности Customer, экземпляр которой находится в источнике данных customerDs.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.addonguidemo.web.customer.CustomerEdit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addonguidemo.web.customer">
<dsContext>
<datasource id="customerDs"
class="com.company.addonguidemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions" spacing="true">
<fieldGroup id="fieldGroup" datasource="customerDs">
<column width="250px">
<field property="name"/>
<field property="score" custom="true"/>
</column>
</fieldGroup>
<frame id="windowActions" screen="editWindowActions"/>
</layout>
</window>
package com.company.addonguidemo.web.customer;
import com.company.addonguidemo.gui.components.Stepper;
import com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.addonguidemo.entity.Customer;
import com.haulmont.cuba.gui.components.FieldGroup;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import javax.inject.Inject;
import java.util.Map;
public class CustomerEdit extends AbstractEditor<Customer> {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private FieldGroup fieldGroup;
@Inject
private Datasource<Customer> customerDs;
@Override
public void init(Map<String, Object> params) {
Stepper stepper = componentsFactory.createComponent(Stepper.class);
stepper.setDatasource(customerDs, "score");
stepper.setWidth("100%");
fieldGroup.getFieldNN("score").setComponent(stepper);
}
}
/* Define your theme modifications inside next mixin */
@mixin com_company_application-halo-ext {
/* Basic styles for stepper inner text box */
.stepper input[type="text"] {
@include box-defaults;
@include valo-textfield-style;
&:focus {
@include valo-textfield-focus-style;
}
}
}
4.7.4.3. Подключение JavaScript библиотеки
В данном примере мы подключим компонент Slider из библиотеки jQuery UI. Слайдер будет иметь два ползунка, определяющих диапазон значений.
Создайте новый проект в CUBA Studio и назовите его jscomponent.
Далее нажмите на кнопку New UI component. Откроется окно создания визуального компонента UI component generation. В секции Component type выберите значение JavaScript component.
В поле Vaadin component class name диалога генерации компонента введите значение SliderServerComponent.
Уберите флажок Integrate into Generic UI. Процесс интеграции компонента в универсальный интерфейс аналогичен описанному в разделе Подключение аддона Vaadin с интеграцией в Generic UI, поэтому рассматривать его здесь мы не будем.
После нажатия кнопки OK Studio сгенерирует файлы:
Последовательно пройдемся по сгенерированным заготовкам файлов и внесем в них необходимые изменения.
package com.company.jscomponent.web.toolkit.ui.slider;
import com.vaadin.shared.ui.JavaScriptComponentState;
public class SliderState extends JavaScriptComponentState {
public double[] values;
public double minValue;
public double maxValue;
}
package com.company.jscomponent.web.toolkit.ui.slider;
import com.vaadin.annotations.StyleSheet;
import com.vaadin.ui.AbstractJavaScriptComponent;
import com.vaadin.annotations.JavaScript;
import elemental.json.JsonArray;
@JavaScript({"slider-connector.js", "jquery-ui.js"})
@StyleSheet({"jquery-ui.css"})
public class SliderServerComponent extends AbstractJavaScriptComponent {
public interface ValueChangeListener {
void valueChanged(double[] newValue);
}
private ValueChangeListener listener;
public SliderServerComponent() {
addFunction("valueChanged", arguments -> {
JsonArray array = arguments.getArray(0);
double[] values = new double[2];
values[0] = array.getNumber(0);
values[1] = array.getNumber(1);
getState(false).values = values;
listener.valueChanged(values);
});
}
public void setValue(double[] value) {
getState().values = value;
}
public double[] getValue() {
return getState().values;
}
public double getMinValue() {
return getState().minValue;
}
public void setMinValue(double minValue) {
getState().minValue = minValue;
}
public double getMaxValue() {
return getState().maxValue;
}
public void setMaxValue(double maxValue) {
getState().maxValue = maxValue;
}
@Override
protected SliderState getState() {
return (SliderState) super.getState();
}
@Override
public SliderState getState(boolean markAsDirty) {
return (SliderState) super.getState(markAsDirty);
}
public ValueChangeListener getListener() {
return listener;
}
public void setListener(ValueChangeListener listener) {
this.listener = listener;
}
}
Серверный компонент Vaadin определяет набор геттеров и сеттеров для работы с состоянием слайдера, а также интерфейс слушателя изменения значений. Класс компонента должен быть унаследован от AbstractJavaScriptComponent.
Метод addFunction() в конструкторе класса объявляет обработчик RPC-вызова функции valueChanged() с клиента.
Аннотации @JavaScript и @StyleSheet указывают на файлы, которые должны быть загружены на веб-страницу. В нашем примере это JavaScript файлы библиотеки jQuery UI и коннектора, а также файл со стилями для jQuery UI. Расположим их в одном Java-пакете с серверным компонентом.
Скачайте архив jQuery UI с сайта http://jqueryui.com/download и поместите файлы jquery-ui.js и jquery-ui.css в пакет с классом SliderServerComponent. На странице скачивания jQuery UI у вас будет возможность выбрать компоненты, которые будут помещены в архив. Для нашего примера достаточно выделить пункт Slider группы Widgets.
com_company_jscomponent_web_toolkit_ui_slider_SliderServerComponent = function() {
var connector = this;
var element = connector.getElement();
$(element).html("<div/>");
$(element).css("padding", "5px 10px");
var slider = $("div", element).slider({
range: true,
slide: function(event, ui) {
connector.valueChanged(ui.values);
}
});
connector.onStateChange = function() {
var state = connector.getState();
slider.slider("values", state.values);
slider.slider("option", "min", state.minValue);
slider.slider("option", "max", state.maxValue);
$(element).width(state.width);
}
}
Коннектор представляет собой функцию, которая при загрузке веб-страницы проинициализирует JavaScript компонент. Имя функции должно соответствовать полному имени класса серверного компонента, где точки в имени пакета заменены на символ подчеркивания.
Vaadin добавляет ряд полезных методов в функцию коннектора. Например, this.getElement() возвращает HTML DOM элемент компонента, this.getState() возвращает объект-состояние.
В нашем примере коннектор делает следующее:
Для демонстрации работы компонента создадим сущность Product с тремя атрибутами:
Затем сгенерируем стандартные экраны для данной сущности. Обратите внимание, что мы используем не универсальный компонент платформы, а "нативный" компонент Vaadin. Следовательно, экраны должны располагаться в модуле Web, а не в GUI - укажите это в окне генерации стандартных экранов.
В редакторе сущности мы хотим устанавливать минимальное и максимальное значение скидки с помощью компонента slider, который мы только что создали.
Перейдите к файлу product-edit.xml. Поля minDiscount и maxDiscount сделайте нередактируемыми, добавив к соответствующим элементам атрибут editable="false". Затем добавьте в fieldGroup новое кастомное поле slider.
В результате XML-дескриптор экрана редактирования должен выглядеть следующим образом:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.jscomponent.web.product.ProductEdit"
datasource="productDs"
focusComponent="fieldGroup"
messagesPack="com.company.jscomponent.web.product">
<dsContext>
<datasource id="productDs"
class="com.company.jscomponent.entity.Product"
view="_local"/>
</dsContext>
<layout expand="windowActions"
spacing="true">
<fieldGroup id="fieldGroup"
datasource="productDs">
<column width="250px">
<field property="name"/>
<field property="minDiscount" editable="false"/>
<field property="maxDiscount" editable="false"/>
<field id="slider" custom="true"/>
</column>
</fieldGroup>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
Перейдите к файлу ProductEdit.java. Замените его содержимое следующим кодом:
package com.company.jscomponent.web.product;
import com.company.jscomponent.web.toolkit.ui.slider.SliderServerComponent;
import com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.jscomponent.entity.Product;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.FieldGroup;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import com.vaadin.ui.Layout;
import javax.inject.Inject;
public class ProductEdit extends AbstractEditor<Product> {
@Inject
private FieldGroup fieldGroup;
@Inject
private ComponentsFactory componentsFactory;
@Inject
private Datasource<Product> productDs;
@Override
protected void initNewItem(Product item) {
super.initNewItem(item);
item.setMinDiscount(15.0);
item.setMaxDiscount(70.0);
}
@Override
protected void postInit() {
super.postInit();
Component box = componentsFactory.createComponent(VBoxLayout.class);
Layout vBox = (Layout) WebComponentsHelper.unwrap(box);
SliderServerComponent slider = new SliderServerComponent();
slider.setValue(new double[]{getItem().getMinDiscount(), getItem().getMaxDiscount()});
slider.setMinValue(0);
slider.setMaxValue(100);
slider.setWidth("240px");
slider.setListener(newValue -> {
getItem().setMinDiscount(newValue[0]);
getItem().setMaxDiscount(newValue[1]);
});
vBox.addComponent(slider);
fieldGroup.getFieldNN("slider").setComponent(box);
}
}
В методе initNewItem() мы проставляем начальные значения скидок для нового продукта.
В методе init() инициализируем кастомное поле для слайдера. Для компонента слайдера мы проставляем текущие значения, максимальное и минимальное значения, а также объявляем слушатель изменений значений. При движении ползунка мы будем проставлять новые значения скидок в соответствующие поля редактируемой сущности.
Запустите проект и откройте экран редактирования продукта. Изменение положения ползунка на слайдере должно изменять значение в соответствующем текстовом поле.
4.7.4.4. Создание GWT компонента
В данном примере мы рассмотрим создание простого GWT-компонента - поля рейтинга в виде 5 звезд, а также использование его в экранах приложения.
Создадим новый проект в CUBA Studio. Имя проекта - ratingsample.
Создайте модуль web-toolkit, нажав на кнопку Create web-toolkit module секции Project properties навигатора Studio.
Далее нажмите на ссылку New UI component. Откроется окно создания визуального компонента UI component generation. В секции Component type выберите значение New GWT component.
В поле Vaadin component class name диалога генерации компонента введите значение RatingFieldServerComponent.
Снимите флажок Integrate into Generic UI. Процесс интеграции компонента в универсальный интерфейс аналогичен описанному в разделе Подключение аддона Vaadin с интеграцией в Generic UI, поэтому рассматривать его здесь мы не будем.
После нажатия кнопки OK Studio сгенерирует файлы:
Последовательно рассмотрим сгенерированные студией заготовки файлов и внесем в них необходимые изменения.
package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.FocusWidget;
import java.util.ArrayList;
import java.util.List;
public class RatingFieldWidget extends FocusWidget {
private static final String CLASSNAME = "ratingfield";
// API for handle clicks
public interface StarClickListener {
void starClicked(int value);
}
protected List<SpanElement> stars = new ArrayList<SpanElement>(5);
protected StarClickListener listener;
protected int value = 0;
public RatingFieldWidget() {
DivElement container = DOM.createDiv().cast();
container.getStyle().setDisplay(Display.INLINE_BLOCK);
for (int i = 0; i < 5; i++) {
SpanElement star = DOM.createSpan().cast();
// add star element to the container
DOM.insertChild(container, star, i);
// subscribe on ONCLICK event
DOM.sinkEvents(star, Event.ONCLICK);
stars.add(star);
}
setElement(container);
setStylePrimaryName(CLASSNAME);
}
// main method for handling events in GWT widgets
@Override
public void onBrowserEvent(Event event) {
super.onBrowserEvent(event);
switch (event.getTypeInt()) {
// react on ONCLICK event
case Event.ONCLICK:
SpanElement element = event.getEventTarget().cast();
// if click was on the star
int index = stars.indexOf(element);
if (index >= 0) {
int value = index + 1;
// set internal value
setValue(value);
// notify listeners
if (listener != null) {
listener.starClicked(value);
}
}
break;
}
}
@Override
public void setStylePrimaryName(String style) {
super.setStylePrimaryName(style);
for (SpanElement star : stars) {
star.setClassName(style + "-star");
}
updateStarsStyle(this.value);
}
// let application code change the state
public void setValue(int value) {
this.value = value;
updateStarsStyle(value);
}
// refresh visual representation
private void updateStarsStyle(int value) {
for (SpanElement star : stars) {
star.removeClassName(getStylePrimaryName() + "-star-selected");
}
for (int i = 0; i < value; i++) {
stars.get(i).addClassName(getStylePrimaryName() + "-star-selected");
}
}
}
Виджет представляет собой клиентский класс, отвечающий за отображение компонента в веб-браузере и реакцию на события. Он определяет интерфейсы для работы с серверной частью. В нашем случае это метод setValue() и интерфейс StarClickListener.
package com.company.ratingsample.web.toolkit.ui;
import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc;
import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState;
import com.vaadin.ui.AbstractField;
// the field will have a value with integer type
public class RatingFieldServerComponent extends AbstractField<Integer> {
public RatingFieldServerComponent() {
// register an interface implementation that will be invoked on a request from the client
registerRpc((RatingFieldServerRpc) value -> setValue(value, true));
}
// field value type
@Override
public Class<? extends Integer> getType() {
return Integer.class;
}
// define own state class
@Override
protected RatingFieldState getState() {
return (RatingFieldState) super.getState();
}
@Override
protected RatingFieldState getState(boolean markAsDirty) {
return (RatingFieldState) super.getState(markAsDirty);
}
// we need to refresh the state when setValue is invoked from the application code
@Override
protected void setInternalValue(Integer newValue) {
super.setInternalValue(newValue);
if (newValue == null) {
newValue = 0;
}
getState().value = newValue;
}
}
package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
import com.vaadin.shared.AbstractFieldState;
public class RatingFieldState extends AbstractFieldState {
{ // change the main style name of the component
primaryStyleName = "ratingfield";
}
// define a field for the value
public int value = 0;
}
package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
import com.vaadin.shared.communication.ServerRpc;
public interface RatingFieldServerRpc extends ServerRpc {
//method will be invoked in the client code
void starClicked(int value);
}
package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc;
import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractFieldConnector;
import com.vaadin.shared.ui.Connect;
// link the connector with the server implementation of RatingField
// extend AbstractField connector
@Connect(RatingFieldServerComponent.class)
public class RatingFieldConnector extends AbstractFieldConnector {
// we will use a RatingFieldWidget widget
@Override
public RatingFieldWidget getWidget() {
RatingFieldWidget widget = (RatingFieldWidget) super.getWidget();
if (widget.listener == null) {
widget.listener = new RatingFieldWidget.StarClickListener() {
@Override
public void starClicked(int value) {
getRpcProxy(RatingFieldServerRpc.class).starClicked(value);
}
};
}
return widget;
}
// our state class is RatingFieldState
@Override
public RatingFieldState getState() {
return (RatingFieldState) super.getState();
}
// react on server state change
@Override
public void onStateChanged(StateChangeEvent stateChangeEvent) {
super.onStateChanged(stateChangeEvent);
// refresh the widget if the value on server has changed
if (stateChangeEvent.hasPropertyChanged("value")) {
getWidget().setValue(getState().value);
}
}
}
Код виджета RatingFieldWidget не определяет внешний вид компонента, кроме назначения имён стилей ключевым элементам. Для того, чтобы определить внешний вид нашего компонента, создадим файлы стилей. Для этого можно воспользоваться ссылкой Create theme extension секции Project properties в навигаторе Studio. В появившемся диалоге выбираем тему halo. Эта тема использует вместо значков глифы шрифта FontAwesome, чем мы и воспользуемся. Studio создаст пустые файлы SCSS для расширения темы в каталоге themes модуля web.
Стили каждого компонента принято выделять в отдельный файл componentname.scss в каталоге components/componentname в формате примеси SCSS. В каталоге themes/halo модуля web создадим структуру вложенных каталогов: components/ratingfield. Затем внутри ratingfield создадим файл ratingfield.scss:
@mixin ratingfield($primary-stylename: ratingfield) {
.#{$primary-stylename}-star {
font-family: FontAwesome;
font-size: $v-font-size--h2;
padding-right: round($v-unit-size/4);
cursor: pointer;
&:after {
content: '\f006'; // 'fa-star-o'
}
}
.#{$primary-stylename}-star-selected {
&:after {
content: '\f005'; // 'fa-star'
}
}
.#{$primary-stylename} .#{$primary-stylename}-star:last-child {
padding-right: 0;
}
.#{$primary-stylename}.v-disabled .#{$primary-stylename}-star {
cursor: default;
}
}
Подключим этот файл в главном файле темы halo-ext.scss:
@import "components/ratingfield/ratingfield";
@mixin com_company_ratingsample-halo-ext {
@include ratingfield;
}
Для демонстрации работы компонента создадим новый экран в модуле web.
Назовите файл с экраном rating-screen.xml.
Перейдем к редактированию экрана rating-screen.xml в IDE. Нам понадобится контейнер для нашего компонента, объявим его в XML экрана:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://caption"
class="com.company.ratingsample.web.screens.RatingScreen"
messagesPack="com.company.ratingsample.web.screens">
<layout expand="container">
<vbox id="container">
<!-- we'll add vaadin component here-->
</vbox>
</layout>
</window>
Откроем класс контроллера экрана RatingScreen.java и добавим код размещения нашего компонента на экране:
package com.company.ratingsample.web.screens;
import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.BoxLayout;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import com.vaadin.ui.Layout;
import javax.inject.Inject;
import java.util.Map;
public class RatingScreen extends AbstractWindow {
@Inject
private BoxLayout container;
@Override
public void init(Map<String, Object> params) {
super.init(params);
com.vaadin.ui.Layout containerLayout = (Layout) WebComponentsHelper.unwrap(container);
RatingFieldServerComponent field = new RatingFieldServerComponent();
field.setCaption("Rate this!");
containerLayout.addComponent(field);
}
}
Запускаем сервер приложения и смотрим на результат.
4.7.4.5. Поддержка собственных компонентов в CUBA Studio
В данном разделе описывается, как добавить поддержку новых визуальных компонентов в дизайнер экранов CUBA Studio. Новый компонент появится в палитре компонентов, вы сможете перетаскивать его в рабочую область и редактировать его свойства в панели свойств компонента.
Рассмотрим процесс создания интеграции компонента stepper, создание которого было описано в разделе Подключение аддона Vaadin с интеграцией в Generic UI.
Откроем проект, содержащий компонент stepper.
|
Если указанный проект вы не создавали, то вы можете воспроизвести шаги, описанные ниже, и на новом проекте. Вы увидите поддержку компонента в Studio, но не сможете запустить приложение. |
Нажмем кнопку Extend Studio на панели Project properties.
Рассмотрим поля, которые необходимо заполнить.
Поля Component class name и Component model class name будут заполнены автоматически на основе введенного значения. Оставьте их значения без изменения.
Выберите caption, datasource и property.
|
Свойства id, align, height, width, enable, stylename, visible по умолчанию доступны для всех компонентов. |
Добавим следующие свойства:
Далее нажмите кнопку OK.
Новые визуальные компоненты инициализируются при старте сервера Studio. Откройте окно сервера Studio, остановите сервер, выйдите из Studio, затем отройте и запустите его снова.
Сгенерируем стандартные экраны для сущности Customer заново, чтобы стереть результаты наших прошлых экспериментов.
Переходим в секцию GENERIC UI навигатора студии и открываем экран customer-edit.
Для начала удалим поле score из fieldGroup, т.к. мы хотим для его редактирования использовать специальный компонент.
На палитре компонентов найдите новый компонент Stepper и перетащите его на экран под fieldGroup.
Выделите компонент stepper и перейдите на закладку свойств компонента Properties.
Заполните необходимые поля:
После этого перейдите на закладку XML, чтобы увидеть результат.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.addonguidemo.gui.customer.CustomerEdit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addonguidemo.gui.customer"
xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd">
<dsContext>
<datasource id="customerDs"
class="com.company.addonguidemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions"
spacing="true">
<fieldGroup id="fieldGroup"
datasource="customerDs">
<column width="250px">
<field property="name"/>
</column>
</fieldGroup>
<app:stepper id="stepper"
caption="Stepper"
datasource="customerDs"
maxValue="50"
property="score"/>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
В XML экрана объявлено пространство имен компонента с префиксом app, компонент stepper добавлен на экран, и у него установлены необходимые свойства.
Данная глава содержит подробное описание архитектуры, компонентов и механизмов платформы.
В данной главе рассмотрена архитектура CUBA-приложений в различных разрезах: по уровням, блокам, модулям и компонентам.
5.1.1. Уровни и блоки приложения
Платформа позволяет строить приложения по классической трехуровневой схеме: клиентский уровень, средний слой, база данных. Уровень отражает степень "удаленности" от хранимых данных.
В дальнейшем речь пойдет в основном о среднем слое и клиентах, поэтому для краткости выражение "все уровни" означает два этих уровня.
На каждом уровне возможно создание одного или нескольких блоков (units) приложения. Блок представляет собой обособленную исполняемую программу, взаимодействующую с другими блоками приложения. Средства платформы CUBA позволяют создавать блоки в виде веб-приложений и десктопных приложений.
Рисунок 6. Уровни и блоки приложения
Middleware
Средний слой, содержащий основную бизнес-логику приложения и выполняющий обращения к базе данных. Представляет собой отдельное веб-приложение под управлением стандартного контейнера Java EE Web Profile. См. Компоненты среднего слоя.
Web Client
Основной блок клиентского уровня. Содержит интерфейс, предназначенный, как правило, для внутренних пользователей организации. Представляет собой отдельное веб-приложение под управлением стандартного контейнера Java EE Web Profile. Реализация пользовательского интерфейса основана на фреймворке Vaadin. См. Универсальный пользовательский интерфейс.
Desktop Client
Дополнительный блок клиентского уровня. Содержит интерфейс, предназначенный, как правило, для внутренних пользователей организации. Представляет собой десктопное Java-приложение, реализация пользовательского интерфейса основана на фреймворке Java Swing. См. Универсальный пользовательский интерфейс.
Web Portal
Дополнительный блок клиентского уровня. Может содержать интерфейс для внешних пользователей и средства интеграции с мобильными устройствами и сторонними приложениями. Представляет собой отдельное веб-приложение под управлением стандартного контейнера Java EE Web Profile. Реализация пользовательского интерфейса основана на фреймворке Spring MVC. См. Компоненты портала.
Polymer Client
Дополнительный клиентский блок на чистом JavaScript, предоставляющий интерфейс для внешних пользователей. Основан на фреймворке Google Polymer, работает со средним слоем через REST API, запущенный в блоке Web Client или Web Portal. См. Пользовательский интерфейс на Polymer.
Обязательным блоком любого приложения является средний слой - Middleware. Для реализации пользовательского интерфейса, как правило, используется один или несколько клиентских блоков, например, Web Client и Web Portal.
Все основанные на Java клиентские блоки взаимодействуют со средним слоем одинаковым образом посредством протокола HTTP, что позволяет размещать средний слой произвольным образом, в том числе за сетевым экраном. Следует отметить, что при развертывании в простейшем случае среднего слоя и веб-клиента на одном сервере между ними организуется локальное взаимодействие в обход сетевого стека для снижения накладных расходов.
Модуль – наименьшая структурная единица CUBA-приложения. Представляет собой один модуль проекта приложения и соответствующий ему JAR файл с исполняемым кодом.
Стандартные модули:
Рисунок 7. Модули приложения
Функциональность платформы разделена на несколько компонентов приложения (ранее называемых базовыми проектами):
Проект приложения всегда зависит от одного или нескольких компонентов. Это означает, что приложение использует компоненты как библиотеки и включает их функциональность.
Любое CUBA-приложение зависит от компонента cuba. Остальные компоненты платформы являются опциональными и могут быть включены в приложение при необходимости. Все опциональные компоненты зависят от cuba и могут также иметь зависимости между собой.
Ниже приведена диаграмма зависимостей между компонентами платформы. Сплошными линиями изображены обязательные зависимости, пунктирными − опциональные.
Любое CUBA-приложение может в свою очередь быть использовано как компонент другого приложения. Это позволяет декомпозировать большие проекты в наборы функциональных модулей, которые могут разрабатываться независимо. Можно также создать в организации набор собственных компонентов использовать их в различных проектах, тем самым создав собственную платформу более высокого уровня на основе CUBA. Ниже приведена диаграмма возможной структуры зависимостей компонентов приложения.
Для того чтобы приложение можно было использовать в качестве компонента, оно должно содержать дескриптор app-component.xml и специальный элемент в манифесте JAR модуля global. CUBA Studio позволяет автоматически сгенерировать дескриптор и манифест для текущего проекта.
Практическое руководство по работе с собственным компонентом приложения приведено в разделе Пример создания и использования компонента.
Вышеописанные архитектурные принципы напрямую отражаются на составе собранного приложения. Рассмотрим его на примере простого приложения sales, которое имеет два блока – Middleware и Web Client; и включает в себя функциональность компонентов cuba и reports.
Рисунок 8. Состав простого приложения
На рисунке изображено содержимое некоторых каталогов сервера Tomcat с развернутым в нем приложением sales.
Блок Middleware реализован веб-приложением app-core, блок Web Client – веб-приложением app. Исполняемый код веб-приложений содержится в каталогах WEB-INF/lib в наборе JAR-файлов. Каждый JAR представляет собой результат сборки (артефакт) одного из модулей приложения или базового проекта.
Например, состав JAR-файлов веб-приложения среднего слоя app-core определяется тем, что блок Middleware состоит из модулей global и core, и приложение использует компоненты cuba и reports.
В данной главе рассмотрены компоненты платформы, общие для всех уровней приложения.
Предметная область моделируется в приложении с помощью взаимосвязанных классов Java, называемых классами сущностей или просто сущностями.
Сущности подразделяются на две категории:
Сущности характеризуются своими атрибутами. Атрибут соответствует полю класса и паре методов доступа (get / set) к полю. Чтобы атрибут был неизменяемым (read only), достаточно не создавать метод set.
Персистентные сущности могут включать в себя атрибуты, не хранящиеся в БД. В случае неперсистентного атрибута можно не создавать поле класса, ограничившись методами доступа.
Класс сущности должен удовлетворять следующим требованиям:
Поддерживаются следующие типы атрибутов сущностей:
Базовые классы сущностей (см. ниже) переопределяют equals() и hashCode() таким образом, что экземпляры сущностей сравниваются по их идентификаторам. То есть экземпляры одного класса считаются равными, если равны их идентификаторы.
5.2.1.1. Базовые классы сущностей
Рассмотрим базовые классы и интерфейсы сущностей более подробно.
|
AbstractInstance хранит слушателей в коллекции WeakReference, т.е. при отсутствии внешних ссылок на добавленного слушателя, он будет немедленно уничтожен сборщиком мусора. Как правило, слушателями изменения атрибутов являются визуальные компоненты и источники данных UI, на которые всегда имеются ссылки из других объектов, поэтому проблема исчезновения слушателей не возникает. Однако если слушатель создается прикладным кодом и на него никто не ссылается естественным образом, необходимо кроме добавления в Instance сохранить его в некотором поле объекта. |
Ниже рассмотрены базовые классы, от которых рекомендуется наследовать сущности. Неперсистентные сущности наследуются от тех же классов, что и персистентные. Фреймворк определяет, является ли сущность персистентной или нет по файлу, в котором зарегистрирован класс: persistence.xml или metadata.xml.
StandardEntity
Наследуйте от StandardEntity, если необходим стандартный набор свойств сущности: первичный ключ типа UUID, экземпляры должны содержать информацию о том, кто и когда создал и изменил их, необходима оптимистичная блокировка и мягкое удаление.
BaseUuidEntity
Наследуйте от BaseUuidEntity, если необходима сущность с идентификатором типа UUID, но не нужны все остальные свойства StandardEntity. Интерфейсы Creatable, Versioned и др. можно выборочно реализовать в конкретном классе сущности.
BaseLongIdEntity
Наследуйте от BaseLongIdEntity или BaseIntegerIdEntity, если необходима сущность с идентификатором типа Long или Integer. Интерфейсы Creatable, Versioned и др. можно выборочно реализовать в конкретном классе сущности. Рекомендуется реализовать HasUuid, так как это позволяет платформе в некоторых случаях работать с сущностью более оптимально, а кроме того, сущность получает уникальный идентификатор в распределенном окружении.
BaseStringIdEntity
Наследуйте от BaseStringIdEntity, если необходима сущность с идентификатором типа String. Интерфейсы Creatable, Versioned и др. можно выборочно реализовать в конкретном классе сущности. Рекомендуется реализовать HasUuid, так как это позволяет платформе в некоторых случаях работать с сущностью более оптимально, а кроме того, сущность получает уникальный идентификатор в распределенном окружении. В конкретном классе сущности, унаследованной от BaseStringIdEntity, необходимо задать атрибут-идентификатор типа String и добавить ему JPA-аннотацию @Id.
BaseIdentityIdEntity
Наследуйте от BaseIdentityIdEntity, если необходимо отобразить сущность на таблицу с первичным ключом типа IDENTITY. Интерфейсы Creatable, Versioned и др. можно выборочно реализовать в конкретном классе сущности. Рекомендуется реализовать HasUuid, так как это позволяет платформе в некоторых случаях работать с сущностью более оптимально, а кроме того, сущность получает уникальный идентификатор в распределенном окружении. Атрибут id сущности (т.е. методы getId/setId) будут иметь тип IdProxy, который предназначен для использования вместо реального идентификатора, пока он не сгенерирован базой данных на вставке записи.
BaseIntIdentityIdEntity
Наследуйте от BaseIntIdentityIdEntity, если необходимо отобразить сущность на таблицу с целочисленным первичным ключом типа IDENTITY (в отличие от Long в BaseIdentityIdEntity). В остальных отношениях BaseIntIdentityIdEntity повторяет BaseIdentityIdEntity.
BaseGenericIdEntity
Наследуйте напрямую от BaseGenericIdEntity, если необходимо отобразить сущность на таблицу с композитным первичным ключом. В этом случае в классе сущности необходимо создать поле встраиваемого типа, представляющего композитный ключ, и аннотировать его JPA-аннотацией @EmbeddedId.
В данном разделе описаны все поддерживаемые платформой аннотации классов и атрибутов сущностей.
Аннотации пакета javax.persistence обеспечивают работу JPA, аннотации пакетов com.haulmont.* предназначены для управления метаданными и другими механизмами платформы.
Если для аннотации указано только простое имя класса, подразумевается что это класс платформы, расположенный в одном из пакетов com.haulmont.*
Определяет встраиваемую сущность, экземпляры которой хранятся вместе с владеющей сущностью в той же таблице.
Для задания имени сущности требуется применение аннотации @MetaClass.
Указывает, что экземпляры данной сущности доступны для восстановления после мягкого удаления в специальном экране core$Entity.restore, доступном через пункт Administration > Data Recovery главного меню.
Объявляет класс сущностью модели данных.
Параметры:
Пример:
@Entity(name = "sales$Customer")
Указывает, что данная сущность является расширением и должна повсеместно использоваться вместо базовой. См. Расширение функциональности.
Используется для определения колонки БД, отвечающей за различение типов сущностей в случае стратегий наследования SINGLE_TABLE и JOINED.
Параметры:
Пример:
@DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.INTEGER)
Определяет значение колонки-дискриминатора для данной сущности. Эта аннотация должна быть помещена на конкретном классе сущности.
Пример:
@DiscriminatorValue("0")
Явно задает имя последовательности базы данных, используемой для генерации идентификаторов сущности, если она является подклассом BaseLongIdEntity или BaseIntegerIdEntity. Если сущность не аннотирована, то фреймворк создает последовательность с автоматически сгенерированным именем.
Параметры:
Определяет стратегию наследования для иерархии классов сущностей. Данная аннотация должна быть помещена на корневом классе иерархии.
Параметры:
Определяет список слушателей, предназначенных для реакции на события жизненного цикла экземпляров сущности на уровне Middleware.
Значением аннотации должна быть строка или массив строк с именами бинов слушателей - см. Entity Listeners.
Примеры:
@Listeners("sample_UserEntityListener")
@Listeners({"sample_FooListener","sample_BarListener"})
Определяет, что данный класс является предком некоторых сущностей, и его атрибуты должны быть использованы в составе сущностей-наследников. Такой класс не сопоставляется никакой отдельной таблице БД.
Используется для объявления неперсистентной или встраиваемой сущности (т.е. когда аннотация @javax.persistence.Entity не применима)
Параметры:
Пример:
@MetaClass(name = "sales$Customer")
Определяет способ получения имени экземпляра, возвращаемого методом Instance.getInstanceName().
Значением аннотации должна быть строка вида {0}|{1}, где
Примеры:
@NamePattern("%s|name")
@NamePattern("#getCaption|login,name")
Данная аннотация может быть указана для метода класса. Такой метод будет вызван сразу после создания экземпляра сущности через Metadata.create(). Это удобно, если для инициализации экземпляра сущности требуется вызов каких-либо бинов. Пример см. в Инициализация полей сущности.
Используется в случае стратегии наследования JOINED для указания колонки внешнего ключа данной сущности, ссылающегося на первичный ключ сущности-предка.
Параметры:
Пример:
@PrimaryKeyJoinColumn(name = "CARD_ID", referencedColumnName = "ID")
Указывает, что данная сущность является системной и не должна быть доступна для выбора пользователем в различных списках сущностей, например, как тип параметра универсального фильтра или тип динамического атрибута.
Определяет таблицу базы данных для данной сущности.
Параметры:
Пример:
@Table(name = "SALES_CUSTOMER")
Указывает, что для данной сущности будет запоминаться история открытия экранов редактирования ({имя_сущности}.edit) с возможностью отображения в специальном экране sec$ScreenHistory.browse, доступном через пункт Help > History главного меню.
5.2.1.2.2. Аннотации атрибутов
Аннотации атрибутов устанавливаются на соответствующие поля класса, за одним исключением: если требуется объявить неизменяемый (read only) неперсистентный атрибут foo, то достаточно создать метод доступа getFoo() и поместить на этот метод аннотацию @MetaProperty.
Применяет автоматическую конвертацию регистра к текстовым полям ввода, связанным с аннотированным атрибутом.
Параметры:
Пример:
@CaseConversion(type = ConversionType.UPPER)
@Column(name = "COUNTRY_CODE")
protected String countryCode;
Определяет колонку БД, в которой будут храниться значения данного атрибута.
Параметры:
Указывает на то, что связь является композицией - более тесным вариантом ассоциации. Это означает, что связанная сущность имеет смысл только как часть владеющей сущности, т.е. создается и удаляется вместе с ней.
Например, список пунктов в заказе (класс Order содержит коллекцию экземпляров Item):
@OneToMany(mappedBy = "order")
@Composition
protected List<Item> items;
Другой пример - one-to-one отношение:
@Composition
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DETAILS_ID")
protected CustomerDetails details;
Указание для связи аннотации @Composition позволяет организовать в экранах редактирования специальный режим коммита источников данных, при котором изменения экземпляров детализирующей сущности сохраняются в базе данных только при коммите основной сущности. Подробнее см. Редактирование композитных сущностей.
Определяет атрибут типа встраиваемой сущности, в свою очередь аннотированной @Embeddable.
Пример:
@Embedded
protected Address address;
По умолчанию ORM не создает экземпляр встроенной сущности если все ее атрибуты равны null в базе данных. Аннотацию @EmbeddedParameters можно использовать для указания того, что экземпляр всегда должен создаваться, например:
@Embedded
@EmbeddedParameters(nullAllowed = false)
protected Address address;
Указывает, что данный атрибут является первичным ключом сущности. Обычно эта аннотация присутствует на поле базового класса, такого как BaseUuidEntity. Использовать эту аннотацию в конкретном классе сущности необходимо только при наследовании от базового класса BaseStringIdEntity (то есть при создании сущности со строковым первичным ключом).
Для атрибутов типа timestamp с аннотацией @javax.persistence.Temporal.TIMESTAMP заставляет платформу игнорировать часовой пояс пользователя, если он задан для текущей сессии.
Используется для указания колонки БД, определяющей ассоциацию между сущностями. Наличие этой аннотации указывает, что данная сторона отношения является владеющей (owning).
Параметры:
Пример:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
Используется для указания таблицы связи на ведущей стороне @ManyToMany ассоциации.
Параметры:
Пример атрибута customers класса Group, являющегося ведущей стороной ассоциации:
@ManyToMany
@JoinTable(name = "SALES_CUSTOMER_GROUP_LINK",
joinColumns = @JoinColumn(name = "GROUP_ID"),
inverseJoinColumns = @JoinColumn(name = "CUSTOMER_ID"))
protected Set<Customer> customers;
Пример атрибута groups класса Customer, являющегося ведомой стороной этой же ассоциации:
@ManyToMany(mappedBy = "customers")
protected Set<Group> groups;
Указывает, что данный атрибут не имеет ограничений длины. Применяется совместно с аннотацией @Column. Если @Lob указан, то длина, заданная в @Column явно или по умолчанию, игнорируется.
Пример:
@Column(name = "DESCRIPTION")
@Lob
private String description;
Служит для описания способа получения локализованного значения некоторого изменяющегося атрибута, которое возвращает метод MessageTools.getLocValue().
Параметры:
Пример аннотации, означающей, что локализованное значение атрибута state будет взято из пакета, имя которого хранится в атрибуте messagesPack связанной сущности proc:
@Column(name = "STATE")
@LocalizedValue(messagePackExpr = "proc.messagesPack")
protected String state;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PROC_ID")
protected Proc proc;
Определяет тип просмотра ссылочных атрибутов.
Параметры:
@Lookup(type = LookupType.DROPDOWN, actions = {"open"})
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
Определяет атрибут-коллекцию ссылок на сущность с типом ассоциации много-ко-многим.
Ассоциация много-ко-многим может иметь ведущую сторону и обратную - ведомую. На ведущей стороне указывается дополнительная аннотация @JoinTable, на ведомой стороне - параметр mappedBy.
Параметры:
|
Использование параметра cascade аннотации не рекомендуется. Сущности, сохраняемые неявно при использовании такого объявления, будут пропущены некоторыми системными механизмами. В частности, бин EntityStates некорректно определяет для них состояние managed, а entity listeners не вызываются вообще. |
Определяет атрибут-ссылку на сущность с типом ассоциации много-к-одному.
Параметры:
Например, несколько экземпляров Order (заказов) ссылаются на один экземпляр Customer (покупателя), в этом случае класс Order должен содержать следующее объявление:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
|
Использование параметра cascade аннотации не рекомендуется. Сущности, сохраняемые неявно при использовании такого объявления, будут пропущены некоторыми системными механизмами. В частности, бин EntityStates некорректно определяет для них состояние managed, а entity listeners не вызываются вообще. |
Указывает, что данный атрибут должен быть включен в метаданные. Данная аннотация может быть установлена как на поле класса, так и на метод доступа, в случае отсутствия соответствующего атрибуту поля.
Данная аннотация не обязательна для полей, снабженных следующими аннотациями пакета javax.persistence: @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany, @Embedded. Такие поля отражаются в метаданных автоматически. Поэтому @MetaProperty в основном применяется для определения неперсистентных атрибутов сущностей.
Параметры (опционально):
Пример использования для поля:
@Transient
@MetaProperty
protected String token;
Пример использования для метода:
@MetaProperty
public String getLocValue() {
if (!StringUtils.isBlank(messagesPack)) {
return AppBeans.get(Messsages.class).getMessage(messagesPack, value);
} else {
return value;
}
}
Задает формат атрибута типа Number (это может быть BigDecimal, Integer, Long или Double). Значения такого атрибута будут форматироваться в пользовательском интерфейсе в соответствии с указанными параметрами аннотации:
Если decimalSeparator и/или groupingSeparator не указаны, фреймворк использует соответствующие значения из format strings для локали текущего пользователя. При форматировании без учета локали в этом случае используются символы из системной локали сервера.
Примеры:
@Column(name = "PRECISE_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "0.0000")
protected BigDecimal preciseNumber;
@Column(name = "WEIRD_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "#,##0.0000", decimalSeparator = "_", groupingSeparator = "`")
protected BigDecimal weirdNumber;
@Column(name = "SIMPLE_NUMBER")
@NumberFormat(pattern = "#")
protected Integer simpleNumber;
@Column(name = "PERCENT_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "#%")
protected BigDecimal percentNumber;
Определяет политику обработки связи в случае мягкого удаления сущности, содержащей данный атрибут. См. Мягкое удаление.
Пример:
@OneToMany(mappedBy = "group")
@OnDelete(DeletePolicy.CASCADE)
private Set<Constraint> constraints;
Определяет политику обработки связи в случае мягкого удаления сущности с обратной стороны ассоциации. См. Мягкое удаление.
Пример:
@ManyToOne
@JoinColumn(name = "DRIVER_ID")
@OnDeleteInverse(DeletePolicy.DENY)
private Driver driver;
Определяет атрибут-коллекцию ссылок на сущность с типом ассоциации один-ко-многим.
Параметры:
Например, несколько экземпляров Item (пунктов заказа) ссылаются на один экземпляр Order (заказ) с помощью @ManyToOne поля Item.order, в этом случае класс Order может содержать коллекцию экземпляров Item:
@OneToMany(mappedBy = "order")
protected Set<Item> items;
|
Использование параметра cascade аннотации не рекомендуется. Сущности, сохраняемые неявно при использовании такого объявления, будут пропущены некоторыми системными механизмами. В частности, бин EntityStates некорректно определяет для них состояние managed, а entity listeners не вызываются вообще. Параметр orphanRemoval не принимает во внимание механизм мягкого удаления. |
Определяет атрибут-ссылку на сущность с типом ассоциации один-к-одному.
Параметры:
Пример ведущей стороны ассоциации, класс Driver:
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CALLSIGN_ID")
protected DriverCallsign callsign;
Пример ведомой стороны ассоциации, класс DriverCallsign:
@OneToOne(fetch = FetchType.LAZY, mappedBy = "callsign")
protected Driver driver;
Определяет порядок элементов в атрибуте-коллекции на момент извлечения из базы данных. Данную аннотацию необходимо задавать для упорядоченных коллекций, таких как List или LinkedHashSet для получения предсказуемого порядка следования элементов.
Параметры:
orderby_list::= orderby_item [,orderby_item]*
orderby_item::= property_or_field_name [ASC | DESC]
Пример:
@OneToMany(mappedBy = "user")
@OrderBy("createTs")
protected List<UserRole> userRoles;
Для атрибута типа java.util.Date уточняет тип хранимого значения: дата, время или дата+время.
Параметры:
Пример:
@Column(name = "START_DATE")
@Temporal(TemporalType.DATE)
protected Date startDate;
Указывает, что данное поле не хранится в БД, т.е. является неперсистентным.
Поля поддерживаемых JPA типов (см. http://docs.oracle.com/javaee/7/api/javax/persistence/Basic.html) по умолчанию являются персистентными, поэтому аннотация @Transient обязательна для объявления неперсистентного атрибута такого типа.
Для включения @Transient атрибута в метаданные, необходимо также указать аннотацию @MetaProperty.
Указывает, что данное поле хранит версию для поддержки оптимистичной блокировки сущностей.
Применение такого поля необходимо при реализации классом сущности интерфейса Versioned (базовый класс StandardEntity уже содержит такое поле).
Пример:
@Version
@Column(name = "VERSION")
private Integer version;
В стандартном варианте использования JPA для атрибутов типа enum в базе данных хранится целое число, получаемое методом ordinal() этого перечисления. Такой подход может привести к следующим проблемам при эксплуатации и развитии системы:
Чтобы решить эти проблемы, в подходе CUBA предлагается отвязать значение, хранимое в БД, от ordinal перечисления. Для этого необходимо поле класса сущности объявлять с типом, хранимым в БД (Integer или String), а методы доступа (getter / setter) создавать для типа самого перечисления.
Например:
@Entity(name = "sales$Customer")
@Table(name = "SALES_CUSTOMER")
public class Customer extends StandardEntity {
@Column(name = "GRADE")
protected Integer grade;
public CustomerGrade getGrade() {
return grade == null ? null : CustomerGrade.fromId(grade);
}
public void setGrade(CustomerGrade grade) {
this.grade = grade == null ? null : grade.getId();
}
...
}
При этом сам класс перечисления может выглядеть следующим образом:
public enum CustomerGrade implements EnumClass<Integer> {
PREMIUM(10),
HIGH(20),
MEDIUM(30);
private Integer id;
CustomerGrade(Integer id) {
this.id = id;
}
@Override
public Integer getId() {
return id;
}
public static CustomerGrade fromId(Integer id) {
for (CustomerGrade grade : CustomerGrade.values()) {
if (grade.getId().equals(id))
return grade;
}
return null;
}
}
Для правильного отражения в метаданных класс перечисления, используемый в качестве типа атрибута сущности, должен реализовывать интерфейс EnumClass.
Как видно из примеров, для атрибута grade в БД хранится значение типа Integer, задаваемое полем id перечисления CustomerGrade, а конкретно 10, 20 или 30. В то же время прикладной код и метаданные работают с самим типом CustomerGrade через методы доступа, которые и осуществляют конвертацию.
При наличии в поле БД значения, не соответствующего ни одному значению перечисления, метод getGrade() просто вернет null. Для ввода нового значения, например, HIGHER, между HIGH и PREMIUM, достаточно добавить это значение в перечисление с идентификатором 15, при этом сортировка по полю Customer.grade останется верной.
Тип поля Integer удобно использовать в случаях, когда необходим упорядоченный список констант, подлежащий сортировке, например, в запросах JPQL и SQL (>, <, >=, ⇐, order by), кроме того, он имеет незначительное преимущество перед String в плане производительности формата хранения и занимаемого места. С другой стороны, значения типа Integer сами по себе неочевидны и могут затруднять отладку и интерпретацию результатов запросов, они неудобны в работе с голыми данными и сериализованными форматами. Если отношение упорядочения между константами не требуется, удобнее использовать тип String.
Перечисления могут быть созданы в CUBA Studio на вкладке DATA MODEL (New → Enumeration). Чтобы использовать перечисление в качестве атрибута сущности, в редакторе атрибута нужно выбрать ENUM в поле Attribute type и класс перечисления в поле Type. Значениям перечисления могут быть сопоставлены локализованные названия для отображения в пользовательском интерфейсе приложения.
Платформа CUBA поддерживает режим "мягкого удаления" данных - когда вместо удаления записей из базы данных они только помечаются определенным образом и становятся недоступными для обычного использования. В дальнейшем такие записи можно либо совсем удалить из БД с помощью отдельной регламентной процедуры, либо восстановить.
Механизм мягкого удаления является "прозрачным" для прикладного программиста - достаточно убедиться, что класс сущности реализует интерфейс SoftDelete, и платформа сама нужным образом будет модифицировать запросы и операции с данными.
Режим мягкого удаления имеет следующие преимущества:
Возьмем для примера модель данных Заказы - Покупатели. Допустим, на некоторого покупателя оформлено несколько заказов, однако нам нужно сделать его недоступным для дальнейшей работы пользователей. Традиционным "жестким" удалением сделать это невозможно, так как для удаления покупателя нам нужно либо удалить все его заказы, либо обнулить в этих заказах ссылки на него (т.е. потерять информацию). При мягком удалении покупателя он становится недоступным для поиска и изменения, однако при просмотре заказов пользователь видит на экране имя покупателя, так как при загрузке связей признак удаления намеренно игнорируется.
Описанное поведение является стандартным, но может быть модифицировано с помощью политики обработки связей при удалении.
Восстановить удалённые сущности можно через экран Restore Deleted Entities, по умолчанию доступный в стандартном меню Administration приложения. Эта функциональность предназначена для использования администраторами системы, имеющими разрешения на все сущности. Её следует использовать с осторожностью, также рекомендуется ограничить доступ к этому экрану для простых пользователей системы.
Отрицательной стороной мягкого удаления является увеличение объема базы данных и потенциальная необходимость дополнительных процедур ее очистки.
Для того чтобы экземпляры сущности удалялись мягко, класс сущности должен реализовывать интерфейс SoftDelete, а соответствующая таблица БД должна содержать колонки:
Поведение системы по умолчанию - сущности, реализующие SoftDelete, удаляются мягко, удаленные сущности не возвращаются запросами и поиском по идентификатору. При необходимости такое поведение можно динамически отключить следующими способами:
В режиме мягкого удаления платформа автоматически отфильтровывает удаленные экземпляры при загрузке по идентификатору и по JPQL-запросу, а также удаленные элементы связанных сущностей в атрибутах-коллекциях. Однако связанные сущности в единичных (*ToOne) атрибутах загружаются независимо от того, удален связанный экземпляр или нет.
5.2.1.4.2. Политика обработки связей
Для мягко удаляемых сущностей, платформа предоставляет средство обработки связей при удалении экземпляров, во многом аналогичное правилам ON DELETE внешних ключей в базе данных. Это средство работает на уровне Middleware и использует аннотации @OnDelete, @OnDeleteInverse атрибутов сущности.
Аннотация @OnDelete обрабатывается при удалении той сущности, в которой она встретилась, а не той, на которую указывает аннотированный атрибут (в этом отличие от каскадных удалений на уровне БД).
Аннотация @OnDeleteInverse обрабатывается при удалении той сущности, на которую указывает аннотированный атрибут, (т.е. аналогично каскадному удалению на уровне внешних ключей в БД). Эта аннотация полезна при отсутствии в удаляемом объекте атрибута, который нужно проверять при удалении. При этом, как правило, в проверяемом объекте существует ссылка на удаляемый, на этот атрибут и устанавливается аннотация @OnDeleteInverse.
Значением аннотации может быть:
Примеры:
Order.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
@OnDeleteInverse(DeletePolicy.DENY)
protected Customer customer;
Customer.java
@OneToMany(mappedBy = "customer")
protected List<Order> orders;
Сообщения в окне исключения могут быть локализованы в главном пакете сообщений. Используйте для этого следующие ключи:
Role.java
@OneToMany(mappedBy = "role")
@OnDelete(DeletePolicy.CASCADE)
protected Set<Permission> permissions;
Permission.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ROLE_ID")
protected Role role;
Role.java
@OneToMany(mappedBy = "role")
protected Set<Permission> permissions;
Permission.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ROLE_ID")
@OnDeleteInverse(DeletePolicy.UNLINK)
protected Role role;
Особенности реализации:
Например, в случае ассоциации Customer - Job и большого количества работ для одного заказчика, если поставить на атрибут Job.customer политику @OnDeleteInverse(CASCADE), то при удалении экземпляра заказчика будет предпринята попытка извлечь и изменить все его работы. Это может привести к перегрузке сервера приложения и БД.
С другой стороны, использование @OnDeleteInverse(DENY) безопасно, так как при этом производится только подсчет количества ссылающихся объектов, и если оно больше 0, выбрасывается исключение. Поэтому @OnDeleteInverse(DENY) для атрибута Job.customer вполне допустимо.
5.2.1.4.3. Ограничение уникальности на уровне БД
В режиме мягкого удаления для ограничения уникальности некоторого значения необходимо обеспечить существование единственной неудаленной записи с этим значением, и произвольного количества удаленных записей с этим же значением.
Реализуется данная логика путем, специфичным для используемого сервера базы данных:
create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC) where DELETE_TS is null
create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC, DELETE_TS)
Для эффективной работы с моделью данных в CUBA-приложениях используется фреймворк метаданных, который:
5.2.2.1. Интерфейсы метаданных
Рассмотрим основные интерфейсы метаданных.
Рисунок 9. Интерфейсы фреймворка метаданных
Session
Точка входа в фреймворк метаданных. Позволяет получать экземпляры MetaClass по имени и по соответствующему классу Java. Обратите внимание на различие методов getClass() и getClassNN() - первые могут возвращать null, вторые нет (NonNull).
Объект Session может быть получен через интерфейс инфраструктуры Metadata.
Пример:
@Inject
protected Metadata metadata;
...
Session session = metadata.getSession();
MetaClass metaClass1 = session.getClassNN("sec$User");
MetaClass metaClass2 = session.getClassNN(User.class);
assert metaClass1 == metaClass2;
MetaModel
Редко используемый интерфейс, служит для группировки мета-классов.
Группировка осуществляется по имени корневого Java пакета проекта, указываемого в файле metadata.xml.
Интерфейс метаданных класса сущности. MetaClass всегда ассоциирован с классом Java, которого он представляет.
Основные методы:
Пример:
MetaClass userClass = session.getClassNN(User.class);
MetaProperty groupProperty = userClass.getPropertyNN("group");
Пример:
MetaClass userClass = session.getClassNN(User.class);
MetaProperty groupNameProp = userClass.getPropertyPath("group.name").getMetaProperty();
assert groupNameProp.getDomain().getName().equals("sec$Group");
Интерфейс метаданных атрибута сущности.
Основные методы:
Вид ссылочного атрибута ASSOCIATION или COMPOSITION влияет на режим редактирования сущности: в первом случае сохранение связанной сущности в базу данных происходит независимо, а во втором − связанная сущность сохраняется в БД только вместе с владеющей сущностью.
Range
Интерфейс, детально описывающий тип атрибута сущности.
Основные методы:
5.2.2.2. Формирование метаданных
Основной источник формирования структуры метаданных - аннотированные классы сущностей.
Класс сущности отражается в метаданных в следующих случаях:
Все сущности внутри одного корневого пакета помещаются в один экземпляр MetaModel, которому присваивается имя этого пакета. Между сущностями внутри одной MetaModel можно устанавливать произвольные связи, между разными - в порядке объявления файлов metadata.xml в свойстве cuba.metadataConfig.
Атрибут сущности отражается в метаданных, если:
Параметры мета-класса и мета-свойств формируются на основе параметров перечисленных аннотаций, а также типов полей и методов класса. Кроме того, если у атрибута отсутствует метод доступа на запись (setter), атрибут становится неизменяемым (read only).
Интерфейс Datatype определяет методы конвертации значение в строку и из строки (formatting & parsing). Каждый атрибут сущности, не являющийся ссылкой, имеет некоторый Datatype, который и используется фреймворком для конвертации значений данного атрибута.
Экземпляры Datatype регистрируются в бине DatatypeRegistry, который выполняет загрузку и инициализацию классов реализации Datatype из файлов metadata.xml компонентов приложения и самого проекта.
Datatype атрибута сущности может быть получен из соответствующего meta-property методом getRange().asDatatype().
Кроме конвертации значений атрибутов сущностей, зарегистрированные экземпляры Datatype могут быть использованы для преобразования в строку и из строки произвольных значений поддерживаемых типов. Для этого необходимо получить экземпляр Datatype из DatatypeRegistry с помощью его методов get(Class) или getNN(Class), передавая тип Java, который необходимо конвертировать.
Datatype сопоставляется атрибуту сущности по следующим правилам:
Например, в данном случае атрибут amount получит BigDecimalDatatype:
@Column(name = "AMOUNT")
private BigDecimal amount;
потому что в cuba-metadata.xml есть следующий элемент:
<datatype id="decimal" class="com.haulmont.chile.core.datatypes.impl.BigDecimalDatatype"
default="true"
format="0.####" decimalSeparator="." groupingSeparator=""/>
Например, атрибут issueYear получит тип YearDatatype:
@MetaProperty(datatype = "year")
@Column(name = "ISSUE_YEAR")
private Integer issueYear;
если файл metadata.xml проекта содержит следующий элемент:
<datatype id="year" class="com.company.sample.YearDatatype"/>
Как видно, атрибут datatype аннотации @MetaProperty указывает на идентификатор, который использован при регистрации класса имплементации Datatype в файле metadata.xml.
Основные методы интерфейса Datatype:
Datatype определяет два набора методов для форматирования/парсинга: с учетом локали и без учета локали. Преобразование с учетом локали используется повсеместно в пользовательском интерфейсе, преобразование без учета локали используется в системных механизмах, например, для сериализации в REST API.
Форматы для преобразований без учета локали задаются в коде имплементации или в файле metadata.xml.
В следующем разделе описано, как задать форматы преобразований с учетом локали.
5.2.2.3.1. Строки форматов Datatype
Форматы для преобразований с учетом локали задаются в главном пакете локализованных сообщений проекта или его компонентов, в строках со следующими ключами:
|
Форматы для используемых в приложении языков могут быть заданы в Studio. Для этого откройте на редактирование Project Properties, нажмите кнопку в поле Available locales, затем нажмите Show data format strings. |
Строки форматов могут быть получены из бина FormatStringsRegistry.
5.2.2.3.2. Пример специализированного Datatype
В качестве примера рассмотрим следующую задачу: в приложении есть атрибуты сущностей, хранящие годы в виде целых чисел. Пользователи должны иметь возможность просматривать и редактировать годы, причем если пользователь вводит только две цифры, приложение должно преобразовать их в год между 2000 и 2100. В противном случае, все введенное число считается годом.
Создайте класс в модуле global:
package com.company.sample.entity;
import com.google.common.base.Strings;
import com.haulmont.chile.core.annotations.JavaClass;
import com.haulmont.chile.core.datatypes.Datatype;
import javax.annotation.Nullable;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;
@JavaClass(Integer.class)
public class YearDatatype implements Datatype<Integer> {
private static final String PATTERN = "##00";
@Override
public String format(@Nullable Object value) {
if (value == null)
return "";
DecimalFormat format = new DecimalFormat(PATTERN);
return format.format(value);
}
@Override
public String format(@Nullable Object value, Locale locale) {
return format(value);
}
@Nullable
@Override
public Integer parse(@Nullable String value) throws ParseException {
if (Strings.isNullOrEmpty(value))
return null;
DecimalFormat format = new DecimalFormat(PATTERN);
int year = format.parse(value).intValue();
if (year > 2100 || year < 0)
throw new ParseException("Invalid year", 0);
if (year < 100)
year += 2000;
return year;
}
@Nullable
@Override
public Integer parse(@Nullable String value, Locale locale) throws ParseException {
return parse(value);
}
}
Затем добавьте элемент datatypes в файл metadata.xml вашего проекта:
<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
<datatypes>
<datatype id="year" class="com.company.sample.entity.YearDatatype"/>
</datatypes>
<!-- ... -->
</metadata>
В элементе datatype можно также указать атрибут sqlType, содержащий SQL-тип вашей базы данных, подходящий для хранения значений нового типа. Этот SQL-тип будет использоваться CUBA Studio при генерации скриптов базы данных. Studio может автоматически определить SQL-тип для следующих типов Java:
В нашем случае класс предназначен для работы с типом Integer (что декларируется аннотацией @JavaClass со значением Integer.class), поэтому атрибут sqlType можно не указывать.
Наконец, укажите новый тип данных для требуемых атрибутов (программно или с помощью интерфейса Studio):
@MetaProperty(datatype = "year")
@Column(name = "ISSUE_YEAR")
private Integer issueYear;
После выполнения перечисленных действий атрибут latitude везде в приложении будет отображаться в нужном формате.
5.2.2.3.3. Пример форматирования даты в UI
Рассмотрим отображение атрибута Order.date в таблице браузера заказов.
order-browse.xml
<table id="ordersTable">
<columns>
<column id="date"/>
<!--...-->
Атрибут date в классе Order определен с типом "дата":
@Column(name = "DATE", nullable = false)
@Temporal(TemporalType.DATE)
private Date date;
Если текущий пользователь зарегистрирован c русской локалью, то из главного пакета локализованных сообщений извлекается строка:
dateFormat=dd.MM.yyyy
В результате дата "2012-08-06" конвертируется в строку "06.08.2012" для отображения в ячейке таблицы.
5.2.2.3.4. Примеры форматирования дат и чисел в коде приложения
Если вам необходимо отформатировать или получить из строки значения BigDecimal, Integer, Long, Double, Boolean или Date учитывая локаль текущего пользователя, используйте бин DatatypeFormatter. Например:
@Inject
private DatatypeFormatter formatter;
void sample() {
String dateStr = formatter.formatDate(dateField.getValue());
// ...
}
Ниже приведены примеры использования методов интерфейса Datatype напрямую.
@Inject
protected UserSessionSource userSessionSource;
@Inject
protected DatatypeRegistry datatypes;
void sample() {
Date date;
// ...
String dateStr = datatypes.getNN(Date.class).format(date, userSessionSource.getLocale());
// ...
}
com/sample/sales/web/messages_ru.properties
coordinateFormat = #,##0.00000
@Inject
protected Messages messages;
@Inject
protected UserSessionSource userSessionSource;
@Inject
protected FormatStringsRegistry formatStringsRegistry;
void sample() {
String coordinateFormat = messages.getMainMessage("coordinateFormat");
FormatStrings formatStrings = formatStringsRegistry.getFormatStrings(userSessionSource.getLocale());
NumberFormat format = new DecimalFormat(coordinateFormat, formatStrings.getFormatSymbols());
String formattedValue = format.format(value);
// ...
}
Мета-аннотации сущностей - набор пар ключ/значение, содержащих дополнительную информацию о сущностях.
Обращение к мета-аннотациям производится с помощью метода мета-класса getAnnotations().
Источниками мета-аннотаций сущности являются:
Пример переопределения мета-аннотаций в metadata.xml:
<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
<!-- ... -->
<annotations>
<entity class="com.company.customers.entity.Customer">
<annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory">
<attribute name="value" value="true" datatype="boolean"/>
</annotation>
<property name="name">
<annotation name="length" value="200"/>
</property>
<property name="customerGroup">
<annotation name="com.haulmont.cuba.core.entity.annotation.Lookup">
<attribute name="type" class="com.haulmont.cuba.core.entity.annotation.LookupType" value="DROPDOWN"/>
<attribute name="actions" datatype="string">
<value>lookup</value>
<value>open</value>
</attribute>
</annotation>
</property>
</entity>
<entity class="com.company.customers.entity.CustomerGroup">
<annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore">
<attribute name="value" value="false" datatype="boolean"/>
</annotation>
</entity>
</annotations>
</metadata>
При извлечении сущностей из базы данных обычно встает вопрос - как обеспечить загрузку связанных сущностей на нужную глубину?
Например, для браузера Заказов нужно отобразить дату и сумму заказа совместно с названием Покупателя, т.е. загрузить связанный экземпляр Покупателя. А для экрана редактирования Заказа необходимо загрузить еще и коллекцию Пунктов заказа, причем каждый Пункт заказа должен содержать связанный экземпляр Товара для отображения его наименования.
Загрузка по требованию в большинстве случаев не может помочь, так как обработка данных, как правило, происходит не в транзакции, в которой загружаются сущности, а, например, на клиентском уровне в пользовательском интерфейсе. В то же время задание жадной загрузки в аннотациях сущностей недопустимо, так как приводит к постоянному извлечению всего графа связанных сущностей, который может быть очень большим.
Другой похожей проблемой является ограничение набора локальных атрибутов сущностей загружаемого графа: например, некоторая сущность имеет 50 атрибутов, в том числе BLOB, а в экране отображается только 10 атрибутов. Зачем загружать из БД, затем сериализовать и передавать клиенту 40 атрибутов, которые ему в данный момент не нужны?
Механизм представлений (views) решает эти проблемы, обеспечивая извлечение из базы данных и передачу клиенту графов сущностей, ограниченных в глубину и по атрибутам. Представление является описателем графа объектов, который требуется в некотором экране UI или другом процессе обработки данных.
Обработка представлений производится следующим образом:
Рисунок 10. Классы представления
Представление определяется экземпляром класса View, в котором:
Данное свойство в некоторой степени контролируется механизмами загрузки данных платформы. См. разделы о загрузке частичных сущностей в DataManager и EntityManager.
Класс ViewProperty имеет следующие свойства:
Если атрибут fetch не указан, будет использоваться режим AUTO. Если атрибут представляет собой кэшируемую сущность, независимо от указанного значения будет использоваться UNDEFINED.
|
Независимо от набора атрибутов, определенного в представлении, всегда загружаются следующие атрибуты:
|
|
При попытке прочитать или установить значение незагруженного (не включенного в представление) атрибута генерируется исключение. Проверить, загружен ли некоторый атрибут можно методом EntityStates.isLoaded(). |
Незагруженные атрибуты имеют значение null. По умолчанию попытка установки значения незагруженного атрибута (вызов setter) для Detached сущности вызывает исключение.
Следует иметь в виду, что незагруженные ссылочные атрибуты Detached сущности, соответствующие внешним ключам (т.е. many-to-one, one-to-one), можно установить в новое ненулевое значение в любом случае, и изменения будут сохранены при последующем merge().
5.2.3.1. Создание представлений
Представление может быть создано двумя путями:
View view = new View(Order.class)
.addProperty("date")
.addProperty("amount")
.addProperty("customer", new View(Customer.class)
.addProperty("name")
);
Как правило, таким способом создаются представления, используемые только в каком-то одном месте бизнес-логики.
Рассмотрим подробнее декларативный способ создания и работы с представлениями.
ViewRepository является бином Spring, доступным для всех блоков приложения. Ссылка на ViewRepository может быть также получена через интерфейс инфраструктуры Metadata. Для получения экземпляра View, содержащегося в репозитории, используются методы getView(). Для развертывания XML-описателей представлений в репозитории используются методы deployViews() базовой реализации AbstractViewRepository.
В репозитории для каждой сущности по умолчанию доступны три представления с именами _local, _minimal и _base:
Подробная структура XML-описателей изложена здесь.
Пример описателя представления для сущности Заказ, которое должно обеспечить загрузку всех локальных атрибутов, ассоциированного Покупателя и коллекции Пунктов заказа:
<view class="com.sample.sales.entity.Order"
name="order-with-customer"
extends="_local">
<property name="customer" view="_minimal"/>
<property name="items" view="itemInOrder"/>
</view>
Рекомендуемый способ группировки и развертывания описателей представлений:
Если на момент развертывания некоторого представления в репозитории уже есть представление для этого же класса сущности и с таким же именем, то новое будет проигнорировано. Для того чтобы представление заменило имеющееся в репозитории и гарантированно было развернуто, в XML-описателе должен быть явно указан атрибут overwrite = "true".
|
Рекомендуется давать представлениям "описательные" имена. Например, не "browse", а "customerBrowse". Это упрощает поиск XML-описателей представлений по имени в процессе разработки приложения. |
Управляемые бины (Managed Beans) − это программные компоненты, предназначенные для реализации бизнес-логики приложения. Термин "управляемые" в данном случае означает, что созданием экземпляров и установкой связей между такими компонентами управляет контейнер который является основной частью фреймворка Spring.
|
Managed Bean представляет собой singleton, то есть в некотором блоке приложения существует только один экземпляр данного класса. Поэтому, если бин содержит изменяемые данные в полях (другими словами, имеет состояние), то обращение к таким данным необходимо синхронизировать. |
Для создания управляемого бина достаточно добавить классу Java аннотацию @org.springframework.stereotype.Component. Например:
package com.sample.sales.core;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Component;
@Component(OrderWorker.NAME)
public class OrderWorker {
public static final String NAME = "sales_OrderWorker";
public void calculateTotals(Order order) {
}
}
Рекомендуется присваивать бину уникальное имя вида {имя_проекта}_{имя_класса}, и определять его в константе NAME.
|
Аннотация @javax.annotation.ManagedBean также может ипользоваться для определения бина, однако ее наличие может вызывать проблемы при развертывании в некоторые сервера приложений. Поэтому мы рекомендуем использовать только аннотацию @Component из Spring Framework. |
Класс управляемого бина должен находиться внутри дерева пакетов с корнем, заданным в элементе context:component-scan файла spring.xml. В нашем случае файл spring.xml содержит элемент:
<context:component-scan base-package="com.sample.sales"/>
что означает, что поиск аннотированных бинов для данного блока приложения будет происходить начиная с пакета com.sample.sales.
Управляемые бины можно создавать на любом уровне, так как контейнер Spring Framework используется во всех стандартных блоках приложения.
Ссылку на бин можно получить с помощью инжекции или класса AppBeans. В качестве примера использования бина рассмотрим реализацию сервиса OrderService, делегирующего выполнение бину OrderWorker:
package com.sample.sales.core;
import com.haulmont.cuba.core.Persistence;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;
@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {
@Inject
protected Persistence persistence;
@Inject
protected OrderWorker orderWorker;
@Transactional
@Override
public BigDecimal calculateTotals(Order order) {
Order entity = persistence.getEntityManager().merge(order);
orderWorker.calculateTotals(entity);
}
}
В данном примере сервис стартует транзакцию, вносит полученный с клиентского уровня экземпляр сущности в персистентный контекст, и передает управление бину OrderWorker, который и содержит основную бизнес-логику.
Иногда требуется предоставить администратору системы возможность просматривать и изменять состояние некоторого управляемого бина во время выполнения. В этом случае рекомендуется создать JMX-бин - программный компонент, имеющий JMX-интерфейс. Такой бин, как правило, делегирует вызовы управляемому бину, содержащему кэш, конфигурационные данные или статистику, к которым нужно обеспечить доступ через JMX.
Как видно из диаграммы, JMX-бин состоит из интерфейса и класса реализации. Класс должен представлять собой управляемый бин, то есть иметь аннотацию @Component и уникальное имя. Интерфейс JMX-бина специальным образом регистрируется в spring.xml для создания в текущей JVM собственно JMX-интерфейса.
Вызовы всех методов интерфейса JMX-бина перехватываются с помощью Spring AOP классом−интерцептором MBeanInterceptor, который обеспечивает установку правильного ClassLoader в контексте потока выполнения, и журналирование необработанных исключений.
|
Интерфейс JMX-бина обязательно должен иметь имя вида {имя_класса}MBean. |
С JMX-интерфейсом можно работать из внешних инструментов, таких как jconsole или jvisualvm. Кроме того, в состав блока Web Client платформы входит JMX-консоль, предоставляющая базовые средства просмотра состояния и вызова методов JMX-бинов.
Рассмотрим процесс создания JMX-бина на примере.
package com.sample.sales.core;
import org.springframework.jmx.export.annotation.*;
@ManagedResource(description = "Performs operations on Orders")
public interface OrdersMBean {
@ManagedOperation(description = "Recalculates an order amount")
@ManagedOperationParameters({@ManagedOperationParameter(name = "orderId", description = "")})
String calculateTotals(String orderId);
}
@JmxRunAsync(timeout = 30000)
String calculateTotals();
Если таймаут превышен, диалог закрывается с сообщением об ошибке.
|
Пожалуйста имейте в виду, что если операция прервана пользователем или по таймауту, она все равно продолжает работать в фоне, то есть данные действия не прерывают самого выполнения, а только возвращают управление пользователю. |
package com.sample.sales.core;
import com.haulmont.cuba.core.*;
import com.haulmont.cuba.core.app.*;
import com.sample.sales.entity.Order;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.UUID;
@Component("sales_OrdersMBean")
public class Orders implements OrdersMBean {
@Inject
protected OrderWorker orderWorker;
@Inject
protected Persistence persistence;
@Authenticated
@Override
public String calculateTotals(String orderId) {
try {
try (Transaction tx = persistence.createTransaction()) {
Order entity = persistence.getEntityManager().find(Order.class, UUID.fromString(orderId));
orderWorker.calculateTotals(entity);
tx.commit();
};
return "Done";
} catch (Throwable e) {
return ExceptionUtils.getStackTrace(e);
}
}
}
Аннотация @Component определяет, что данный класс является управляемым бином с именем sales_OrdersMBean. Имя указано напрямую в аннотации, а не в константе, так как доступ к JMX-бину из кода Java не требуется.
Рассмотрим реализацию метода calculateTotals().
В данном случае все исключения обрабатываются, а значит, не попадают в MBeanInterceptor и не выводятся в журнал автоматически. Поэтому при необходимости логировать исключения здесь нужно добавить вызов логгера в секции catch.
<bean id="sales_MBeanExporter" lazy-init="false"
class="com.haulmont.cuba.core.sys.jmx.MBeanExporter">
<property name="beans">
<map>
<entry key="${cuba.webContextName}.sales:type=Orders"
value-ref="sales_OrdersMBean"/>
</map>
</property>
</bean>
Все JMX-бины проекта объявляются в одном экземпляре MBeanExporter в элементах map/entry свойства beans. Ключом элемента здесь является JMX ObjectName, значением - имя бина, заданное в аннотации @Component. ObjectName начинается с имени веб-приложения, так как в одном экземпляре сервера приложения (т.е. в одной JVM) может быть развернуто несколько веб-приложений, экспортирующих одинаковые JMX-интерфейсы.
В данном разделе описаны некоторые имеющиеся в платформе JMX-бины.
CachingFacadeMBean предоставляет методы очистки различных кэшей в блоках Middleware и Web Client.
JMX ObjectName: app-core.cuba:type=CachingFacade и app.cuba:type=CachingFacade
ConfigStorageMBean позволяет просматривать и задавать значения свойствам приложения в блоках Middleware, Web Client и Web Portal.
Данный интерфейс имеет отдельные наборы операций для работы с параметрами конфигурации и развертывания (*AppProperties) и с параметрами времени выполнения (*DbProperties). Эти операции отображают только свойства, явно заданные в хранилище. То есть если имеется конфигурационный интерфейс, определяющий некоторое свойство и его значение по умолчанию, но в базе данных или в файлах никакого значения не указано, данные операции не отобразят свойство и его текущее значение.
Заметьте, что изменения в свойствах, хранящихся в файлах, не персистентны и актуальны только до перезапуска блока приложения.
В отличие от операций, описанных выше, операция getConfigValue() всегда возвращает в точности то значение, какое вернул бы соответствующий метод конфигурационного интерфейса, вызванный из кода приложения.
JMX ObjectName:
EmailerMBean позволяет просмотреть текущие значения параметров отсылки email, а также отправить тестовое сообщение.
JMX ObjectName: app-core.cuba:type=Emailer
5.2.5.2.4. PersistenceManagerMBean
PersistenceManagerMBean предоставляет следующие возможности:
JMX ObjectName: app-core.cuba:type=PersistenceManager
5.2.5.2.5. ScriptingManagerMBean
ScriptingManagerMBean является JMX-фасадом для интерфейса инфраструктуры Scripting.
JMX ObjectName: app-core.cuba:type=ScriptingManager
JMX-атрибуты:
JMX-операции:
Для отображения в JMX-интерфейсе результат должен быть типа String. В остальном аналогичен методу Scripting.runGroovyScript().
Пример скрипта, создающего набор тестовых пользователей:
import com.haulmont.cuba.core.*
import com.haulmont.cuba.core.global.*
import com.haulmont.cuba.security.entity.*
PasswordEncryption passwordEncryption = AppBeans.get(PasswordEncryption.class)
Transaction tx = persistence.createTransaction()
try {
EntityManager em = persistence.getEntityManager()
Group group = em.getReference(Group.class, UUID.fromString('0fa2b1a5-1d68-4d69-9fbd-dff348347f93'))
for (i in (1..250)) {
User user = new User()
user.setGroup(group)
user.setLogin("user_${i.toString().padLeft(3, '0')}")
user.setName(user.login)
user.setPassword(passwordEncryption.getPasswordHash(user.id, '1'));
em.persist(user)
}
tx.commit()
} finally {
tx.end()
}
ServerInfoMBean предоставляет общую информацию о данном блоке Middleware: номер и дату сборки, идентификатор сервера.
JMX ObjectName: app-core.cuba:type=ServerInfo
5.2.6. Интерфейсы инфраструктуры
Интерфейсы инфраструктуры обеспечивают доступ к часто используемой функциональности платформы. Большинство из этих интерфейсов расположены в модуле global и могут быть использованы как на среднем слое, так и в блоках клиентского уровня, но некоторые (например, Persistence) доступны только коду среднего слоя.
Интерфейсы инфраструктуры реализуются бинами Spring Framework, поэтому они могут быть инжектированы в любые другие управляемые компоненты (Managed Beans, сервисы среднего слоя, контроллеры экранов универсального пользовательского интерфейса).
Кроме того, как и любые другие бины, интерфейсы инфраструктуры могут быть получены с помощью статических методов класса AppBeans и использоваться в неуправляемых компонентах (POJO, вспомогательных классах и пр.).
Позволяет получать ссылки на конфигурационные интерфейсы там, где невозможна их инжекция.
Пример:
// field injection
@Inject
protected Configuration configuration;
...
String tempDir = configuration.getConfig(GlobalConfig.class).getTempDir();
// setter injection
protected GlobalConfig globalConfig;
@Inject
public void setConfiguration(Configuration configuration) {
this.globalConfig = configuration.getConfig(GlobalConfig.class);
}
// location
String tempDir = AppBeans.get(Configuration.class).getConfig(GlobalConfig.class).getTempDir();
Интерфейс Messages обеспечивает получение локализованных строк сообщений.
Рассмотрим методы интерфейса подробнее.
Примеры:
@Inject
protected Messages messages;
...
String message1 = messages.getMessage(getClass(), "someMessage");
String message2 = messages.getMessage("com.abc.sales.web.customer", "someMessage");
String message3 = messages.getMessage(RoleType.STANDARD);
Пример:
String formattedValue = messages.formatMessage(getClass(), "someFormat", someValue);
Пример:
protected Messages messages = AppBeans.get(Messages.class);
...
messages.getMainMessage("actions.Ok");
Пример:
String formattedValue = messages.formatMessage(messages.getMainMessagePack(), "someFormat", someValue);
ManagedBean, содержащий вспомогательные методы работы с локализованными сообщениями. Интерфейс MessageTools можно получить либо методом Messages.getTools(), либо как любой другой бин - инжекцией или через класс AppBeans.
Методы MessageTools:
Составные части ссылки:
Примеры ссылок на сообщения:
msg://someMessage
msg://com.abc.sales.web.customer/someMessage
Для расширения набора вспомогательных методов в конкретном приложении бин MessageTools можно переопределить. Примеры работы с расширенным интерфейсом:
MyMessageTools tools = messages.getTools();
tools.foo();
((MyMessageTools) messages.getTools()).foo();
Интерфейс Metadata обеспечивает доступ к сессии метаданных и репозиторию представлений.
Методы интерфейса:
Для персистентных наследников BaseLongIdEntity и BaseIntegerIdEntity данный метод также присваивает идентификаторы. Значения идентификаторов получаются из автоматически создаваемых в базе данных последовательностей. По умолчанию последовательности создаются в основном хранилище. Если же свойство приложения cuba.useEntityDataStoreForIdSequence установлено в true, последовательности будут создаваться в хранилище, к которому принадлежит данная сущность.
ManagedBean, содержащий вспомогательные методы работы с метаданными. Интерфейс MetadataTools можно получить либо методом Metadata.getTools(), либо как любой другой бин - инжекцией или через класс AppBeans.
Методы MetadataTools:
Для расширения набора вспомогательных методов в конкретном приложении бин MetadataTools можно переопределить. Примеры работы с расширенным интерфейсом:
MyMetadataTools tools = metadata.getTools();
tools.foo();
((MyMetadataTools) metadata.getTools()).foo();
Обеспечивает загрузку ресурсов по следующим правилам:
На практике явное указание URL или префикса classpath: используется редко, т.е. обычно ресурсы загружаются либо из конфигурационного каталога, либо из classpath. Ресурс в конфигурационном каталоге замещает одноименный ресурс в classpath.
Методы Resources:
@Inject
protected Resources resources;
...
InputStream stream = null;
try {
stream = resources.getResourceAsStream(resourceLocation);
...
} finally {
IOUtils.closeQuietly(stream);
}
Возможно использование "try with resources":
try (InputStream stream = resources.getResourceAsStream(resourceLocation)) {
...
}
Интерфейс Scripting позволяет динамически (т.е. во время работы приложения) компилировать и загружать классы Java и Groovy, а также выполнять скрипты и выражения на Groovy.
Методы Scripting:
Свойство приложения cuba.groovyEvaluatorImport позволяет определить общий набор импортируемых классов, подставляемых в каждое выполняемое выражение. По умолчанию все стандартные блоки приложения импортируют класс PersistenceHelper.
Скомпилированные выражения кэшируются, что значительно ускоряет повторное выполнение.
Пример:
@Inject
protected Scripting scripting;
...
Integer intResult = scripting.evaluateGroovy("2 + 2", new Binding());
Binding binding = new Binding();
binding.setVariable("instance", new User());
Boolean boolResult = scripting.evaluateGroovy("return PersistenceHelper.isNew(instance)", binding);
Скрипт должен быть расположен либо в конфигурационном каталоге приложения, либо в classpath (текущая реализация Scripting поддерживает ресурсы classpath только внутри JAR-файлов). Скрипт в конфигурационном каталоге замещает одноименный скрипт в classpath.
Путь к скрипту указывается с разделителями /, в начале пути символ / не требуется.
Пример:
@Inject
protected Scripting scripting;
...
Binding binding = new Binding();
binding.setVariable("itemId", itemId);
BigDecimal amount = scripting.runGroovyScript("com/abc/sales/CalculatePrice.groovy", binding);
Файлы исходных текстов Java и Groovy в каталоге конфигурации можно изменять во время работы приложения. При следующем вызове loadClass() соответствующий класс будет перекомпилирован и возвращен новый, однако существуют следующие ограничения:
Пример:
@Inject
protected Scripting scripting;
...
Class calculatorClass = scripting.loadClass("com.abc.sales.PriceCalculator");
Кэш скомпилированных классов можно очистить во время выполнения с помощью JMX-бина CachingFacadeMBean.
См. также ScriptingManagerMBean.
Обеспечивает авторизацию - проверку прав пользователя на различные объекты системы. Перед вызовом соответствующих методов UserSession выполняется поиск исходного мета-класса сущности, что является важным при наличии расширений. Кроме методов, дублирующих методы UserSession, данный интерфейс имеет методы isEntityAttrReadPermitted() и isEntityAttrUpdatePermitted(), предназначенные для определения доступности пути к атрибуту с учетом доступности атрибутов и сущностей, входящих в этот путь.
Интерфейс Security рекомендуется использовать в прикладном коде вместо вызовов методов UserSession.isXYXPermitted().
Подробнее см. Аутентификация пользователей.
Обеспечивает получение текущего времени. Применение new Date() и т.п. в прикладном коде не рекомендуется.
Примеры:
@Inject
protected TimeSource timeSource;
...
Date date = timeSource.currentTimestamp();
long startTime = AppBeans.get(TimeSource.class).currentTimeMillis();
Обеспечивает получение объекта сессии текущего пользователя. Подробнее см. Аутентификация пользователей.
Обеспечивает получение значений UUID, в том числе для идентификаторов сущностей. Применение UUID.randomUUID() в прикладном коде не рекомендуется.
Для вызова из статического контекста можно использовать класс UuidProvider, который имеет также дополнительный метод fromString(), работающий быстрее, чем стандартный метод UUID.fromString().
Интерфейс DataManager является универсальным средством для загрузки графов сущностей из базы данных, и для сохранения изменений, произведенных в detached экземплярах сущностей.
|
В разделе DataManager vs. EntityManager приведена информация о различиях между DataManager и EntityManager. |
DataManager на самом деле делегирует выполнение реализациям DataStore, и поддерживает ссылки между сущностями из разных хранилищ. Большинство деталей реализации, описанных ниже, актуальны только когда производится работа через RdbmsStore с сущностями, хранящимися в реляционной БД. Для другого типа хранилища все, кроме сигнатур методов, может отличаться. Для простоты изложения, далее, когда мы говорим просто DataManager, мы будем иметь в виду DataManager через RdbmsStore.
Методы DataManager:
@Inject
private DataManager dataManager;
private Book loadBookById(UUID bookId) {
return dataManager.load(Book.class).id(bookId).view("book.edit").one();
}
private List<BookPublication> loadBookPublications(UUID bookId) {
return dataManager.load(BookPublication.class)
.query("select p from library$BookPublication p where p.book.id = :bookId")
.parameter("bookId", bookId)
.view("bookPublication.full")
.list();
}
List<KeyValueEntity> list = dataManager.loadValues(
"select o.customer, sum(o.amount) from demo$Order o " +
"where o.date >= :date group by o.customer")
.properties("customer", "sum")
.parameter("date", orderDate)
.list();
BigDecimal sum = dataManager.loadValue(
"select sum(o.amount) from demo$Order o " +
"where o.date >= :date group by o.customer", BigDecimal.class)
.parameter("date", orderDate)
.one();
@Inject
private DataManager dataManager;
private Book loadBookById(UUID bookId) {
LoadContext<Book> loadContext = LoadContext.create(Book.class)
.setId(bookId).setView("book.edit");
return dataManager.load(loadContext);
}
private List<BookPublication> loadBookPublications(UUID bookId) {
LoadContext<BookPublication> loadContext = LoadContext.create(BookPublication.class)
.setQuery(LoadContext.createQuery("select p from library$BookPublication p where p.book.id = :bookId")
.setParameter("bookId", bookId))
.setView("bookPublication.full");
return dataManager.loadList(loadContext);
}
ValueLoadContext context = ValueLoadContext.create()
.setQuery(ValueLoadContext.createQuery(
"select o.customer, sum(o.amount) from demo$Order o " +
"where o.date >= :date group by o.customer")
.setParameter("date", orderDate))
.addProperty("customer")
.addProperty("sum");
List<KeyValueEntity> list = dataManager.loadValues(context);
Метод возвращает набор экземпляров сущностей, возвращенных из метода EntityManager.merge(), то есть по сути свежие экземпляры, только что обновленные в БД. Дальнейшая работа должна производиться именно с этими возвращенными экземплярами, чтобы предотвратить потерю данных или исключения оптимистичной блокировки. Для того, чтобы обеспечить наличие нужных атрибутов у возвращенных сущностей, с помощью мэп CommitContext.getViews() можно указать представление для каждого сохраняемого экземпляра.
Примеры сохранения коллекций сущностей:
@Inject
private DataManager dataManager;
private void saveBookInstances(List<BookInstance> toSave, List<BookInstance> toDelete) {
CommitContext commitContext = new CommitContext(toSave, toDelete);
dataManager.commit(commitContext);
}
private Set<Entity> saveAndReturnBookInstances(List<BookInstance> toSave, View view) {
CommitContext commitContext = new CommitContext();
for (BookInstance bookInstance : toSave) {
commitContext.addInstanceToCommit(bookInstance, view);
}
return dataManager.commit(commitContext);
}
Например, если вы создаете экземпляр сущности User, вам необходимо установить ссылку на Group, в которую данный пользователь будет входить. Если вам известен id группы, то вы могли бы загрузить данную группу из БД. Данный метод позволяет получить экземпляр Group без ненужного обращения к БД:
user.setGroup(dataManager.getReference(Group.class, groupId));
dataManager.commit(user);
Ссылка может также быть использована для удаления существующего объекта по идентификатору:
dataManager.remove(dataManager.getReference(Customer.class, customerId));
Правила создания запросов аналогичны описанным в разделе Выполнение JPQL-запросов. Отличием является то, что в запросе, выполняемом через DataManager, могут быть использованы только именованные параметры, позиционные не поддерживаются.
DataManager всегда стартует новую транзакцию и по завершении работы выполняет коммит, таким образом возвращая сущности в detached состоянии.
Частичная сущность - это экземпляр сущности, в котором может быть загружена только часть локальных атрибутов. По умолчанию, DataManager загружает частичные сущности в соответствии с указанными представлениями. (на самом деле, RdbmsStore просто устанавливает свойство loadPartialEntities у представления в true и передает его дальше в EntityManager).
В некоторых случаях DataManager загружает все локальные атрибуты и представление определяет только загрузку связей:
5.2.6.10.1. Права доступа в DataManager
Методы load(), loadList(), loadValues() и getCount() проверяют наличие у пользователя права READ на загружаемую сущность. Кроме того, при извлечении сущностей из БД накладываются ограничения групп доступа.
Метод commit() проверяет наличие у пользователя права UPDATE на изменяемые сущности и DELETE на удаляемые.
По умолчанию, DataManager проверяет права на операции (READ/CREATE/UPDATE/DELETE) с сущностями, когда вызывается с клиентской стороны, и игнорирует их, когда вызывается из кода middleware. Права на атрибуты по умолчанию не проверяются.
Если вы хотите, чтобы DataManager проверял права на операции и при вызове на среднем слое, получите методом DataManager.secure() специальный объект-обертку и вызывайте методы у него. В качестве альтернативы, вы можете установить свойство приложения cuba.dataManagerChecksSecurityOnMiddleware, чтобы проверка прав работала для всего приложения.
Права на атрибуты будут проверяться на среднем слое только если вы дополнительно установите свойство приложения cuba.entityAttributePermissionChecking в true. Это имеет смысл, если средний слой обслуживает также клиентов, которые теоретически могут быть взломаны, например, десктоп-клиент. В этом случае, установите также свойство cuba.keyForSecurityTokenEncryption в уникальное значение. Если ваше приложение использует только Web или Portal клиентов, то оба данных свойства можно безопасно оставить со значениями по умолчанию.
Имейте в ввиду, что ограничения групп доступа (row-level security) применяются всегда, независимо от того, был ли вызов с клиентского или со среднего слоя.
5.2.6.10.2. Запросы с distinct
В JPQL запросах для экранов со списками сущностей, в которых включено постраничное отображение и возможна непредсказуемая модификация запроса универсальным фильтром или механизмом ограничений групп доступа, при отсутствии в запросе оператора distinct может возникать следующий эффект:
Таким образом, рекомендуется в JPQL запросы браузеров включать предложение distinct, которое гарантирует отсутствие дубликатов записей при выборке из базы данных. Однако в некоторых серверах БД (в частности PostgreSQL) при большом количестве извлекаемых записей (более 10000) SQL запрос с distinct выполняется недопустимо долго.
Для решения этой проблемы в платформе реализована возможность корректной работы без distinct на уровне SQL. Данный механизм включается свойством приложения cuba.inMemoryDistinct, при активации которого выполняется следующее:
5.2.6.10.3. Последовательная выборка
DataManager может выполнять последовательную выборку данных из результатов предыдущего запроса. Эта возможность используется в универсальном фильтре при последовательном наложении фильтров.
Данный механизм работает следующим образом:
Таблица SYS_QUERY_RESULT периодически очищается от ненужных результатов запросов, оставленных завершенными пользовательскими сессиями. Для этого предназначен метод deleteForInactiveSessions бина QueryResultsManagerAPI, который вызывается шедулером Spring, объявленным в cuba-spring.xml. По умолчанию это происходит раз в 10 минут, но вы можете задать другой интервал в миллисекундах, используя свойство приложения cuba.deleteOldQueryResultsInterval в модуле core.
Интерфейс для получения информации о персистентных сущностях, сохраняемых через ORM. В отличие от бинов Persistence и PersistenceTools доступен на всех уровнях приложения.
Методы EntityStates:
Вспомогательный класс со статическими методами, делегирующий выполнение бину EntityStates.
Бин Events реализует функциональность публикации объектов-событий уровня приложения. События могут использоваться для передачи данных между слабо связанными компонентами приложения. Бин Events является простым фасадом для объекта ApplicationEventPublisher Spring Framework.
public interface Events {
String NAME = "cuba_Events";
void publish(ApplicationEvent event);
}
Этот бин имеет только один метод - publish(), принимающий объект события. Метод Events.publish() уведомляет все слушатели, зарегистрированные в приложении и подписанные на события того же типа, что и переданный объект. Вы может использовать класс-обёртку PayloadApplicationEvent для публикации любых объектов в качестве событий.
См. также руководство Spring Framework.
Обработка событий в компонентах приложения
Прежде всего, необходимо создать класс события. Он должен быть наследником класса ApplicationEvent. Класс события может включать любые дополнительные данные. Например:
package com.company.sales.core;
import com.haulmont.cuba.security.entity.User;
import org.springframework.context.ApplicationEvent;
public class DemoEvent extends ApplicationEvent {
private User user;
public DemoEvent(Object source, User user) {
super(source);
this.user = user;
}
public User getUser() {
return user;
}
}
Бины могут публиковать события, используя бин Events:
package com.company.sales.core;
import com.haulmont.cuba.core.global.Events;
import com.haulmont.cuba.core.global.UserSessionSource;
import com.haulmont.cuba.security.global.UserSession;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
@Component
public class DemoBean {
@Inject
private Events events;
@Inject
private UserSessionSource userSessionSource;
public void demo() {
UserSession userSession = userSessionSource.getUserSession();
events.publish(new DemoEvent(this, userSession.getUser()));
}
}
По умолчанию все события обрабатываются синхронно.
Есть два способа обработки событий:
В первом случае, мы должны создать бин, реализующий интерфейс ApplicationListener с указанием типа события:
@Component
public class DemoEventListener implements ApplicationListener<DemoEvent> {
@Inject
private Logger log;
@Override
public void onApplicationEvent(DemoEvent event) {
log.debug("Demo event is published");
}
}
Второй способ может использоваться для сокрытия деталей реализации обработчика событий и обработки множества различных событий в одном бине:
@Component
public class MultipleEventListener {
@Order(10)
@EventListener
protected void handleDemoEvent(DemoEvent event) {
// handle event
}
@Order(1010)
@EventListener
protected void handleUserLoginEvent(UserLoggedInEvent event) {
// handle event
}
}
|
По умолчанию, события в Spring требуют модификаторов доступа protected, package или public для методов, аннотированных @EventListener. Обратите внимание, что модификатор private не поддерживается. |
|
Методы, аннотированные @EventListener, не работают в services, JMX-бинах и других бинах с интерфейсами. При использовании @EventListener в таком бине на старте приложения будет выброшено исключение: BeanInitializationException: Failed to process @EventListener annotation on bean. Need to invoke method declared on target class, but not found in any interface(s) of the exposed proxy type. Either pull the method up to an interface or switch to CGLIB proxies by enforcing proxy-target-class mode in your configuration. Если необходимо обрабатывать событие в бине с интерфейсом, реализуйте в нем также интерфейс ApplicationListener с типом нужного события. |
Вы можете использовать интерфейс Ordered и аннотацию @Order Spring Framework для указания порядка исполнения обработчиков событий. Все бины и обработчики событий платформы используют значение order от 100 до 1000, таким образом, вы можете добавить обработчик событий как до, так и после обработчиков события платформы. Если вы хотите добавить свой обработчик события до обработчиков из платформы, то используйте значение меньше 100.
См. также События логина.
Обычно, бин Events делегирует публикацию события объекту ApplicationContext. Для блоков Web Client / Desktop Client это поведение отличается от стандартного, вы можете использовать дополнительный интерфейс для классов событий - UiEvent. Это интерфейс-маркер для событий, которые должны быть доставлены в экраны пользовательского интерфейса текущего экземпляра UI (текущей вкладки веб-браузера). Важно отметить, что экземпляры событий, реализующих UiEvent, не доставляются в бины Spring и не могут быть обработаны за пределами UI.
Пример класса события:
package com.company.sales.web;
import com.haulmont.cuba.gui.events.UiEvent;
import com.haulmont.cuba.security.entity.User;
import org.springframework.context.ApplicationEvent;
public class UserRemovedEvent extends ApplicationEvent implements UiEvent {
private User user;
public UserRemovedEvent(Object source, User user) {
super(source);
this.user = user;
}
public User getUser() {
return user;
}
}
События публикуются при помощи бина Events из контроллера экрана так же, как и из бинов Spring:
@Inject
Events events;
// ...
UserRemovedEvent event = new UserRemovedEvent(this, removedUser);
events.publish(event);
Чтобы обработать событие, вы должны объявить в экране метод с аннотацией @EventListener (Интерфейс ApplicationListener не поддерживается):
@Order(15)
@EventListener
protected void onUserRemove(UserRemovedEvent event) {
showNotification("User is removed " + event.getUser());
}
Вы можете использовать аннотацию @Order, чтобы задать порядок вызова обработчиков события.
Если класс события реализует UiEvent, и объект такого события опубликован при помощи бина Events из потока UI, то будут вызваны обработчики событий этого типа в открытых на данный момент окнах и фреймах. Обработка событий синхронная. Только экраны и фреймы текущей активной вкладки веб-браузера получат уведомление о событии.
AppContext - системный класс, в статических полях которого хранятся ссылки на некоторые общие для любого блока приложения компоненты:
AppContext инициализируется на запуске приложения классами-загрузчиками, специфичными для типа блока приложения:
AppContext может быть использован в прикладном коде для решения следующих задач:
AppContext.addListener(new AppContext.Listener() {
@Override
public void applicationStarted() {
System.out.println("Application is ready");
}
@Override
public void applicationStopped() {
System.out.println("Application is closing");
}
});
|
Рекомендуемый способ выполнения кода в момент запуска и остановки приложения - это использование События жизненного цикла. |
5.2.8. События жизненного цикла
В приложении на CUBA существуют следующие типы событий жизненного цикла:
AppContextInitializedEvent
Посылается сразу после инициализации AppContext. В этот момент:
AppContextStartedEvent
Посылается после AppContextInitializedEvent и после запуска всех AppContext.Listener.applicationStarted(). В этот момент:
AppContextStoppedEvent
Посылается перед остановкой приложения и после запуска всех AppContext.Listener.applicationStopped(). В этот момент:
Порядком исполнения слушателей можно управлять с помощью аннотации @Order. Константы Events.HIGHEST_PLATFORM_PRECEDENCE и Events.LOWEST_PLATFORM_PRECEDENCE определяют диапазон значений, используемый слушателями платформы.
Пример:
package com.company.demo.core;
import com.haulmont.cuba.core.global.Events;
import com.haulmont.cuba.core.sys.events.*;
import org.slf4j.Logger;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
@Component
public class MyAppLifecycleBean {
@Inject
private Logger log;
// event type is defined by annotation parameter
@EventListener(AppContextInitializedEvent.class)
// run after all platform listeners
@Order(Events.LOWEST_PLATFORM_PRECEDENCE + 100)
protected void appInitialized() {
log.info("Initialized");
}
// event type is defined by method parameter
@EventListener
protected void appStarted(AppContextStartedEvent event) {
log.info("Started");
}
@EventListener
protected void appStopped(AppContextStoppedEvent event) {
log.info("Stopped");
}
}
ServletContextInitializedEvent
Посылается сразу после инициализации контекстов ServletContext и AppContext. В этот момент:
Посылается перед уничтожением контекстов ServletContext и AppContext и позволяет вручную освободить ресурсы.
Пример использования:
@Component
public class MyInitializerBean {
@Inject
private Logger log;
@EventListener
public void foo(ServletContextInitializedEvent e) {
log.info("Application and servlet context is initialized");
}
@EventListener
public void bar(ServletContextDestroyedEvent e) {
log.info("Application is about to shut down, all contexts are now destroyed");
}
}
Свойства приложения − именованные значения различных типов, определяющие всевозможные аспекты конфигурации и функционирования приложения. Свойства приложения широко используются в платформе, и могут применяться в приложении для решения аналогичных задач.
По назначению свойства приложения можно классифицировать следующим образом:
Например: cuba.springContextConfig.
Например: cuba.connectionUrlList, cuba.dbmsType, cuba.userSessionExpirationTimeoutSec.
Например: cuba.entityLog.enabled, cuba.email.smtpHost.
Значения свойств приложения могут быть заданы в базе данных, в файлах свойств, или через системные свойства Java. Кроме того, значение, заданное в файле, переопределяет одноименное значение, заданное в БД. Значение, заданное системным свойством Java, переопределяет одноименные значения из файлов и из БД.
Некоторые свойства не поддерживают установку свойств в базе данных по причине того, что их значения требуются еще то того, как БД становится доступной приложению. Это параметры конфигурации и развертывания. Поэтому их можно устанавливать только в файлах свойств или через системные свойства Java. Параметры времени выполнения всегда могут быть установлены в базе данных (и, возможно, переопределены в файле или системными свойствами).
Как правило, некоторое свойство используется только в одном или нескольких блоках приложения. Например, cuba.persistenceConfig необходимо только для Middleware, cuba.web.appWindowMode − только для Web Client, а cuba.springContextConfig − для всех блоков. Это означает, что если нужно задать значение некоторому свойству, это необходимо сделать во всех блоках, в которых данное свойство используется. Свойства, хранящиеся в БД, доступны всем блокам, поэтому они устанавливаются в одном месте (в таблице базы данных), независимо от того, в каких блоках они используются. Более того, платформа предоставляет экран Administration > Application Properties для управления свойствами, хранящимися в БД. Свойства, хранящиеся в файлах, должны быть установлены одновременно в соответствующих файлах блоков приложения.
|
Когда вам необходимо установить значение свойству приложения, определенному платформой, найдите это свойство в документации. Если в документации сказано, что свойство хранится в БД, для установки значения используйте экран Administration > Application Properties. В противном случае выясните в документации, какие блоки приложения используют свойство, и установите значение в файлах app.properties этих блоков. Например, если в документации сказано, что свойство используется во всех блоках, а ваше приложение состоит из Middleware и Web Client, установите свойство в файле app.properties модуля core и в файле web-app.properties модуля web. Параметры развертывания можно также установить вне проекта в конфигурационном каталоге. Подробнее см. Хранение свойств в файлах. |
Свойства из компонентов приложения
Компонент приложения может предоставлять свойства путем объявления их в файле app-component.xml. Тогда если приложение, использующее компонент, не задает собственное значение свойства, значение будет получено из компонента. Если приложение использует несколько компонентов, предоставляющих одно и то же свойство, значение будет получено из компонента, который является ближайшим предком в иерархии зависимостей между компонентами. Если существует несколько компонентов на одном уровне иерархии, то значение свойства непредсказуемо.
Иногда необходимо получить объединенное значение свойства из всех компонентов, используемых в приложении. Это особенно актуально для конфигурационных параметров, которые позволяют механизмам платформы конфигурировать приложение на основании свойств, предоставляемых компонентами приложения.
Такие свойства должны быть сделаны аддитивными путем добавления знака плюс в начале значения. Этот знак говорит о том, что значение свойства во время выполнения должно быть собрано из компонентов приложения. Например, cuba.persistenceConfig - аддитивное свойство. В вашем проекте оно задает файл persistence.xml, определяющий модель данных проекта. Однако вследствие того, что реальное значение свойства будет также включать файлы persistence.xml компонентов приложения, полная модель данных вашего приложения будет включать также и сущности, определенные в компонентах.
Если знак + опустить, то значение будет получено только из текущего проекта. Это может быть полезным в случае, если наследование некоторой конфигурации из компонентов не требуется, например, при определении структуры главного меню.
Значение аддитивного свойства, полученное во время выполнения, состоит из отдельных значений, разделенных пробелом.
Программный доступ к свойствам приложения
Доступ к свойствам из кода приложения можно получить следующими способами:
5.2.9.1. Хранение свойств в файлах
Свойства, определяющие конфигурацию и параметры развертывания, задаются в специальных файлах свойств, имеющих имя вида *app.properties. Каждый блок приложения имеет набор таких файлов, который определяется следующим образом:
Например, набор файлов свойств блока Middleware задается в файле web/WEB-INF/web.xml модуля core, и выглядит следующим образом:
<context-param>
<param-name>appPropertiesConfig</param-name>
<param-value>
classpath:com/company/sample/app.properties
/WEB-INF/local.app.properties
"file:${catalina.base}/conf/app-core/local.app.properties"
</param-value>
</context-param>
Здесь префикс classpath: означает, что данный файл нужно искать в Java classpath, префикс file: − в файловой системе. Путь без такого префикса означает путь внутри веб-приложения относительно его корня. Возможно использование системных свойств Java, в данном случае это catalina.home − путь к каталогу установки Tomcat.
Порядок перечисления файлов важен, так как значения, указанные в каждом последующем файле, заменяют значения одноименных свойств, заданные в предыдущих файлах.
Последний файл в приведенном наборе − local.app.properties. Он может использоваться для переопределения свойств приложения при развертывании. Если этого файла нет, он игнорируется. Если же во время инсталляции системы требуется переопределение некоторых параметров (как правило, различных URL), достаточно создать этот файл и поместить в него переопределяемые свойства. При последующих обновлениях системы такой файл с локальными настройками легко сохранить. В разделе Использование Tomcat при эксплуатации приложения приведен пример использования файла local.app.properties.
Аналогом local.app.properties для Desktop Client служат аргументы командной строки запуска JVM. Загрузчик свойств данного блока воспринимает все аргументы, содержащие знак "=", как пары ключ-значение, и заменяет ими соответствующие свойства приложения, заданные в файлах app.properties.
|
Правила задания информации в файлах *.properties:
|
5.2.9.2. Хранение свойств в базе данных
Свойства приложения, представляющие собой параметры времени выполнения, хранятся в таблице SYS_CONFIG базы данных.
Такие свойства имеют следующие особенности:
Следует иметь в виду, что на клиентском уровне чтение свойства, хранящегося в БД, приводит к запросу к Middleware, что менее эффективно, чем чтение свойства из локального файла app.properties. Для уменьшения количества таких запросов клиент кэширует все свойства, хранящиеся в БД, на время жизни экземпляра реализации конфигурационного интерфейса. Поэтому если, например, в некотором экране UI необходимо несколько раз обратиться к свойствам одного конфигурационного интерфейса, лучше получить ссылку на него при инициализации экрана, и сохранить в поле для последующих обращений к одному и тому же экземпляру.
5.2.9.3. Конфигурационные интерфейсы
Данный механизм позволяет работать со свойствами приложения через методы Java-интерфейсов, что дает следующие преимущества:
Пример получения значения таймаута транзакции в блоке Middleware:
@Inject
private ServerConfig serverConfig;
public void doSomething() {
int timeout = serverConfig.getDefaultQueryTimeoutSec();
...
}
При невозможности инжекции можно получить ссылку на конфигурационный интерфейс через Configuration:
int timeout = AppBeans.get(Configuration.class)
.getConfig(ServerConfig.class)
.getDefaultQueryTimeoutSec();
|
Конфигурационные интерфейсы не являются нормальными бинами Spring, не пытайтесь получить их через AppBeans.get() - только непосредственной инжекцией самого интерфейса или через Configuration.getConfig(). |
Для создания конфигурационного интерфейса необходимо:
Например:
@Source(type = SourceType.DATABASE)
public interface SalesConfig extends Config {
@Property("sales.companyName")
String getCompanyName();
@Property("sales.ftpPassword")
@Secret
String getFtpPassword();
}
Создавать класс реализации конфигурационного интерфейса не нужно - при получении ссылки на интерфейс инжекцией или через Configuration будет автоматически создан необходимый прокси-объект.
"Из коробки" платформой поддерживаются следующие типы свойств:
Если перечисление реализует интерфейс EnumClass и имеет статический метод fromId() для получения значения по идентификатору, с помощью аннотации @EnumStore можно задать хранение значения в виде идентификатора. Например:
@Property("myapp.defaultCustomerGrade")
@DefaultInteger(10)
@EnumStore(EnumStoreMode.ID)
CustomerGrade getDefaultCustomerGrade();
@EnumStore(EnumStoreMode.ID)
void setDefaultCustomerGrade(CustomerGrade grade);
Для поддержки произвольного типа необходимо реализовать классы TypeStringify и TypeFactory для преобразования значения в строку и из нее, и указать эти классы для свойства с помощью аннотаций @Stringify и @Factory.
Рассмотрим этот процесс на примере типа UUID.
public Object build(String string) {
if (string == null) {
return null;
}
return UUID.fromString(string);
}
@Factory(factory = UuidTypeFactory.class)
UUID getUuidProp();
void setUuidProp(UUID value);
В платформе определены реализации TypeFactory для следующих типов:
cuba.test.dateProp = 2013-12-12 00:00:00.000
cuba.test.integerListProp = 1 2 3
cuba.test.stringListProp = aaa|bbb|ccc
5.2.9.3.3. Значения по умолчанию
Для свойств конфигурационных интерфейсов могут быть заданы значения по умолчанию. Эти значения будут возвращаться вместо null, если данный параметр не задан в месте хранения - в БД или в файле app.properties.
Значение по умолчанию может быть задано в виде строки с помощью аннотации @Default, либо в виде конкретного типа с помощью других аннотаций пакета com.haulmont.cuba.core.config.defaults:
@Property("cuba.email.adminAddress")
@Default("address@company.com")
String getAdminAddress();
@Property("cuba.email.delayCallCount")
@Default("2")
int getDelayCallCount();
@Property("cuba.email.defaultSendingAttemptsCount")
@DefaultInt(10)
int getDefaultSendingAttemptsCount();
@Property("cuba.test.dateProp")
@Default("2013-12-12 00:00:00.000")
@Factory(factory = DateFactory.class)
Date getDateProp();
@Property("cuba.test.integerList")
@Default("1 2 3")
@Factory(factory = IntegerListTypeFactory.class)
List<Integer> getIntegerList();
@Property("cuba.test.stringList")
@Default("aaa|bbb|ccc")
@Factory(factory = StringListTypeFactory.class)
List<String> getStringList();
Для сущностей значение по умолчанию задается строкой вида {entity_name}-{id}-{optional_view_name}, например:
@Default("sec$User-98e5e66c-3ac9-11e2-94c1-3860770d7eaf-browse")
User getAdminUser();
@Default("sec$Role-a294aef0-3ac9-11e2-9433-3860770d7eaf")
Role getAdminRole();
Приложение на основе платформы CUBA поддерживает локализацию сообщений, то есть вывод всех элементов пользовательского интерфейса на языке, выбранном пользователем.
Возможности выбора языка пользователем определяются комбинацией свойств приложения cuba.localeSelectVisible и cuba.availableLocales.
Для того, чтобы некоторое сообщение могло быть локализовано, т.е. представлено пользователю на нужном языке, его необходимо поместить в так называемый пакет сообщений. Ниже рассмотрены принципы работы механизма локализации и правила создания сообщений.
Получение локализованных сообщений содержит информацию о способах получения локализованных сообщений в различных компонентах системы.
Пакет сообщений представляет собой набор файлов свойств с именами вида messages{_XX}.properties, расположенных в одном Java-пакете. Суффикс XX определяет язык, для которого в данном файле содержатся сообщения, и соответствует коду языка в Locale.getLanguage(). Возможно также использование остальных атрибутов Locale, например, country. В этом случая файл пакета будет иметь вид messages{XX_YY}.properties. Один из файлов пакета может быть без суффикса языка - это _файл по умолчанию. Именем пакета сообщений считается имя Java-пакета, в котором расположены файлы пакета.
Рассмотрим пример:
/com/abc/sales/gui/customer/messages.properties
/com/abc/sales/gui/customer/messages_fr.properties
/com/abc/sales/gui/customer/messages_ru.properties
/com/abc/sales/gui/customer/messages_en_US.properties
Данный пакет состоит из 4 файлов - один для русского языка, один для французского, один для американского английского (с кодом страны US) и один по умолчанию. Имя пакета - com.abc.sales.gui.customer
Файлы сообщений содержат пары ключ-значение, где ключ - это идентификатор сообщения, на который ссылается код приложения, а значение - само сообщение на языке данного файла. Правила задания пар аналогичны правилам файлов свойств java.util.Properties, со следующими особенностями:
@include=com.haulmont.cuba.web, com.abc.sales.web
someMessage=Some Message
...
Получение сообщений из пакетов производится с помощью методов интерфейса Messages по следующим правилам:
|
Рекомендуется организовывать пакеты сообщений следующим образом:
|
5.2.10.2. Главный пакет сообщений
Каждый стандартный блок приложения определяет для себя один главный пакет сообщений. Для блоков клиентского уровня этот пакет содержит названия пунктов главного меню и общих элементов UI (например, названия кнопок OK и Cancel). Для всех блоков приложения, включая Middleware, главный пакет определяет форматы преобразований Datatype.
Для указания главного пакета сообщений используется свойство приложения cuba.mainMessagePack. Значением свойства может быть либо один пакет, либо список пакетов, разделенный пробелами. Например:
cuba.mainMessagePack=com.haulmont.cuba.web com.abc.sales.web
В данном случае сообщения, заданные во втором пакете списка, будут перекрывать сообщения из первого пакета. Таким образом, в проекте приложения можно переопределять сообщения, заданные в пакетах компонентов приложения.
Сообщения, заданные в пакетах базовых проектов CUBA, также можно переопределять в главном пакете сообщений проекта:
com.haulmont.cuba.gui.backgroundwork/backgroundWorkProgress.timeoutMessage = Новое сообщение об ошибке
5.2.10.3. Локализация названий сущностей и атрибутов
Для отображения в UI локализованных названий сущностей и их атрибутов необходимо создать специальные пакеты сообщений в тех же Java-пакетах, что и сами сущности. Формат файлов сообщений должен быть следующим:
Пример русской локализации сущности com.abc.sales.entity.Customer - файл /com/abc/sales/entity/messages_ru.properties:
Customer=Покупатель
Customer.name=Имя
Customer.email=Email
Order=Заказ
Order.customer=Покупатель
Order.date=Дата
Order.amount=Сумма
Такие пакеты сообщений, как правило, используются неявно для разработчика, например, визуальными компонентами Table и FieldGroup. Кроме того, названия сущностей и атрибутов могут быть также получены следующими методами:
caption="msg://com.abc.sales.entity/Customer.name"
Для локализации названий и значений перечислений необходимо в пакет сообщений, находящийся в Java-пакете класса перечисления добавить сообщения со следующими ключами:
Например, для перечисления
package com.abc.sales;
public enum CustomerGrade { PREMIUM, HIGH, STANDARD }
файл русской локализации /com/abc/sales/messages_ru.properties должен содержать строки:
CustomerGrade=Уровень покупателя
CustomerGrade.PREMIUM=Премиум
CustomerGrade.HIGH=Высокий
CustomerGrade.STANDARD=Стандартный
Локализованные значения перечислений автоматически используются различными визуальными компонентами, например, LookupField. Для программного получения локализованного значения перечисления можно использовать метод getMessage() интерфейса Messages, просто передавая в него экземпляр enum.
5.2.11. Аутентификация пользователей
В данном разделе рассмотрены некоторые аспекты управления доступом с точки зрения разработчика приложения. Для получения полной информации о возможностях и настройке ограничения доступа пользователей к данным см. Подсистема безопасности.
Основной элемент подсистемы контроля доступа в CUBA-приложении - пользовательская сессия. Это объект класса UserSession, который ассоциирован с аутентифицированным в данный момент в системе пользователем, и содержит информацию о правах доступа пользователя к данным. Объект текущей сессии может быть получен в любом блоке приложения через интерфейс инфраструктуры UserSessionSource.
Пользовательская сессия создается на Middleware при выполнении метода AuthenticationManager.login() после аутентификации пользователя по переданному имени и паролю. Объект UserSession затем кэшируется в данном блоке Middleware, и возвращается на клиентский уровень. При работе в кластере объект сессии реплицируется на соседние узлы кластера Middleware. Клиентский блок, получив объект сессии, также сохраняет его у себя, так или иначе ассоциируя с активным пользователем (например, в HTTP сессии). Далее все вызовы Middleware для данного пользователя сопровождаются передачей идентификатора сессии (типа UUID), причем прикладному коду не нужно об этом заботиться - идентификатор сессии передается автоматически, независимо от сигнатуры вызываемых методов среднего слоя. Обработка вызовов клиентов на Middleware начинается с извлечения из кэша сессии по полученному идентификатору и установки ее в потоке выполнения. Объект сессии удаляется из кэша при вызове метода LoginService.logout(), либо при истечении времени бездействия, определяемого свойством приложения cuba.userSessionExpirationTimeoutSec.
Таким образом, идентификатор сессии, создаваемой при входе пользователя в систему, служит для аутентификации пользователя при каждом вызове среднего слоя.
Объект UserSession содержит также методы для авторизации текущего пользователя, т.е. проверки его прав на объекты системы: isScreenPermitted(), isEntityOpPermitted(), isEntityAttrPermitted(), isSpecificPermitted().
С объектом UserSession могут быть ассоциированы именованные атрибуты произвольного сериализуемого типа. Атрибуты устанавливаются методом setAttribute() и возвращаются методом getAttribute(). Последний может также возвращать следующие параметры сессии, как если бы они были атрибутами:
Атрибуты реплицируются в кластере Middleware так же, как и все остальные данные сессии.
Платформа предоставляет встроенные механизмы аутентификации, функциональность которых может быть расширена в приложениях. Они включают в себя различные схемы аутентификации, такие как вход по паролю, функциональность "Запомнить меня", доверенный и анонимный вход в систему.
Данный раздел преимущественно описывает механизмы аутентификации среднего слоя. Для информации об аутентификации веб-клиента см. Процесс входа в Web Client.
Платформа включает следующие механизмы среднего слоя:
Рисунок 11. Механизмы аутентификации среднего слоя
Также, платформа включает следующие дополнительные компоненты:
Основной интерфейс аутентификации - AuthenticationManager, включающий 4 метода:
public interface AuthenticationManager {
AuthenticationDetails authenticate(Credentials credentials) throws LoginException;
AuthenticationDetails login(Credentials credentials) throws LoginException;
UserSession substituteUser(User substitutedUser);
void logout();
}
Здесь есть два метода с похожей ответственностью: authenticate() и login(). Оба метода проверяют, являются ли переданные аутентификационные данные валидными и соответствуют ли они активному пользователю системы, затем возвращают объект AuthenticationDetails. Основное отличие в работе этих методов в том, что метод login() активирует сессию пользователя, так что она может использоваться в дальнейшем для вызова сервисов.
Объект Credentials представляет собой набор аутентификационных данных. Платформа поставляет несколько типов аутентификационных данных:
Доступные на всех слоях:
Доступные только на среднем слое:
Методы login / authenticate AuthenticationManager возвращают объект AuthenticationDetails, который содержит объект UserSession. Этот объект может быть использован для проверки дополнительных разрешений, чтения свойств объекта User и атрибутов сессии. В платформе есть встроенная реализация интерфейса AuthenticationDetails - SimpleAuthenticationDetails, который хранит только объект сессии пользователя, приложения могут предоставлять свою реализацию AuthenticationDetails с дополнительной инфомацией для приложений-клиентов.
AuthenticationManager может выполнить метод authenticate() с одним из следующих результатов:
Реализация AuthenticationManager по умолчанию - AuthenticationManagerBean, который делегирует аутентификацию цепочке экземпляров AuthenticationProvider. AuthenticationProvider - это модуль аутентификации, который может обрабатывать объекты Credentials определённого типа. AuthenticationProvider также имеет специальный метод supports() позволяющий узнать поддерживается ли переданный тип аутентификационных данных.
Рисунок 12. Стандартный процесс входа пользователя
Стандартный процесс входа пользователя:
См. также Специфика процесса входа в Web Client.
Алгоритм хэширования паролей реализуется бином типа EncryptionModule и задается в свойстве приложения cuba.passwordEncryptionModule. По умолчанию - SHA-1.
Встроенные провайдеры аутентификации
Платформа включает следующие реализации интерфейса AuthenticationProvider:
Все реализации загружают пользователя из базы данных, проверяют переданные аутентификационные данные и создают неактивный экземпляр пользовательской сессии при помощи UserSessionManager. Этот экземпляр может стать активным позднее, если вызывается метод AuthenticationManager.login().
Бины LoginPasswordAuthenticationProvider, RememberMeAuthenticationProvider и TrustedClientAuthenticationProvider используют дополнительные подключаемые проверки: бины, реализующие интерфейс UserAccessChecker. Если по крайней мере одна из таких проверок выбрасывает исключение LoginException, то аутентификация не выполняется и исключение LoginException передаётся вызывающему коду.
Кроме того, LoginPasswordAuthenticationProvider и RememberMeAuthenticationProvider проверяют аутентификационные данные при помощи бинов UserCredentialsChecker. Имеется одна встроенная реализация этого интерфейса - BruteForceUserCredentialsChecker, который проверяет, пытается ли пользователь подобрать верные аутентификационные данные при помощи атаки brute-force.
AuthenticationManager и AuthenticationProvider могут выбрасывать исключение LoginException или одного из его наследников из методов authenticate() и login(). Исключение UnsupportedCredentialsException выбрасывается, если переданный объект аутентификационных данных Credentials не может быть обработан имеющимися AuthenticationProvider.
См. следующие классы исключений:
Стандартная реализация AuthenticationManager - AuthenticationManagerBean публикует следующие события во время аутентификации / входа пользователей:
Бины Spring на среднем слое приложения могут обрабатывать эти события при помощи механизма подписок @EventListener:
@Component
public class LoginEventListener {
@Inject
private Logger log;
@EventListener
protected void onUserLoggedIn(UserLoggedInEvent event) {
User user = event.getSource().getUser();
log.info("Logged in user {}", user.getInstanceName());
}
}
Обработчики всех событий, перечисленных выше (кроме AfterLoginEvent, UserSubstitutedEvent и UserLoggedInEvent), могут выбросить LoginException, чтобы прервать процесс аутентификации / входа пользователя.
Например, мы можем реализовать механизм режима обслуживания системы, который позволит запретить вход в систему на время её обслуживания.
@Component
public class MaintenanceModeValve {
private volatile boolean maintenance = true;
public boolean isMaintenance() {
return maintenance;
}
public void setMaintenance(boolean maintenance) {
this.maintenance = maintenance;
}
@EventListener
protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException {
if (maintenance && event.getCredentials() instanceof AbstractClientCredentials) {
throw new LoginException("Sorry, system is unavailable");
}
}
}
Вы можете расширить механизм аутентификации, используя следующие точки расширения:
Вы можете заменить существующие бины, задействуя механизмы Spring Framework, например, зарегистрировав новую реализацию в XML конфигурации Spring модуля core:
<bean id="cuba_LoginPasswordAuthenticationProvider"
class="com.company.authext.core.CustomLoginPasswordAuthenticationProvider"/>
public class CustomLoginPasswordAuthenticationProvider extends LoginPasswordAuthenticationProvider {
@Inject
public CustomLoginPasswordAuthenticationProvider(Persistence persistence, Messages messages) {
super(persistence, messages);
}
@Override
public AuthenticationDetails authenticate(Credentials credentials) throws LoginException {
LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) credentials;
// for instance, add new check before login
if ("demo".equals(loginPassword.getLogin())) {
throw new LoginException("Demo account is disabled");
}
return super.authenticate(credentials);
}
}
Обработчики событий могут быть упорядочены при помощи аннотации @Order. Все бины и обработчики событий платформы используют значение order из диапазона 100 и 1000, что позволяет добавлять обработчики на уровне проект как до, так и после кода платформы. Если вы хотите добавить свой обработчик до обработчиков/бинов платформы - используйте значение меньше 100.
Задание order для обработчика события:
@Component
public class DemoEventListener {
@Inject
private Logger log;
@Order(10)
@EventListener
protected void onUserLoggedIn(UserLoggedInEvent event) {
log.info("Demo");
}
}
Бины AuthenticationProvider могут реализовать интерфейс Ordered и метод getOrder() для определения очерёдности исполнения.
@Component
public class DemoAuthenticationProvider extends AbstractAuthenticationProvider
implements AuthenticationProvider, Ordered {
@Inject
private UserSessionManager userSessionManager;
@Inject
public DemoAuthenticationProvider(Persistence persistence, Messages messages) {
super(persistence, messages);
}
@Nullable
@Override
public AuthenticationDetails authenticate(Credentials credentials) throws LoginException {
// ...
}
@Override
public boolean supports(Class<?> credentialsClass) {
return LoginPasswordCredentials.class.isAssignableFrom(credentialsClass);
}
@Override
public int getOrder() {
return 10;
}
}
Дополнительным плюсом входа в систему для автоматического процесса и для JMX-вызова является то, что вывод в журнал сообщений от логгеров сопровождается указанием логина текущего пользователя, если пользовательская сессия установлена в потоке выполнения. Это упрощает поиск сообщений от конкретного процесса при разборе журнала.
Вход в систему для процессов внутри Middleware выполняется вызовом AuthenticationManager.loginSystem() с передачей объекта SystemUserCredentials , содержащего логин пользователя (без пароля), от имени которого будет работать данный процесс. В результате создается объект UserSession, который будет закэширован в данном блоке Middleware и не будет реплицироваться в кластере.
Более подробно аутентификация процессов внутри Middleware рассмотрена в разделе Системная аутентификация.
Следующие компоненты считаются устаревшими:
Не используйте эти компоненты в коде приложений. Они будут удалены в одной из следующих версий платформы.
Экземпляр класса SecurityContext хранит информацию о пользовательской сессии для текущего потока выполнения. Он создается и передается в метод AppContext.setSecurityContext() в следующие моменты:
По окончании выполнения запроса в первых двух случаях SecurityContext удаляется из потока выполнения.
При создании прикладным кодом нового потока выполнения в него необходимо передать текущий экземпляр SecurityContext, например:
final SecurityContext securityContext = AppContext.getSecurityContext();
executor.submit(new Runnable() {
public void run() {
AppContext.setSecurityContext(securityContext);
// business logic here
}
});
То же самое можно сделать, используя обертки SecurityContextAwareRunnable или SecurityContextAwareCallable, например:
executor.submit(new SecurityContextAwareRunnable<>(() -> {
// business logic here
}));
Future<String> future = executor.submit(new SecurityContextAwareCallable<>(() -> {
// business logic here
return some_string;
}));
В данном разделе рассмотрены различные аспекты генерации и обработки исключений в CUBA-приложениях.
При создании собственных классов исключений следует придерживаться следующих правил:
Платформа содержит специальный класс недекларируемого исключения SilentException, который можно использовать для прерывания хода выполнения без выдачи каких-либо сообщений пользователю или в лог. SilentException объявлен в модуле global, поэтому доступен как на Middleware, так и в клиентских блоках.
5.2.12.2. Передача исключений Middleware
Если при выполнении запроса от клиента на Middleware возникает исключение, выполнение прерывается и на клиента возвращается объект исключения, как правило, включающий цепочку порождающих друг друга исключений. Так как цепочка исключений может содержать классы, недоступные клиентскому блоку (например, исключения JDBC-драйвера), на клиента передается не сама эта цепочка, а ее представление внутри специального создаваемого исключения RemoteException.
Информация об исключениях-причинах сохраняется в виде списка объектов RemoteException.Cause. Каждый объект Cause хранит обязательно имя класса исключения и его сообщение. Кроме того, если класс исключения "поддерживается клиентом", то Cause содержит также и сам объект исключения. Это дает возможность передать на клиента информацию в полях исключения.
Класс исключения, объекты которого нужно передавать на клиентский уровень именно в виде Java-объектов, нужно аннотировать @SupportedByClient, например:
@SupportedByClient
public class WorkflowException extends RuntimeException {
...
Таким образом, при возникновении на Middleware исключения, не аннотированного @SupportedByClient, вызывающий клиентский код получит RemoteException, внутри которого будет находиться исходное исключение в виде строки. Если же исходное исключение аннотировано @SupportedByClient, то вызывающий код получит именно его. Это дает возможность в прикладном коде организовывать обработку декларируемых сервисами Middleware исключений традиционным образом - с помощью блоков try/catch.
Следует иметь в виду, что чтобы поддерживаемое клиентом исключение было действительно передано на клиента в виде объекта, оно не должно содержать внутри себя в цепочке getCause() неподдерживаемых исключений. Поэтому если вы создаете на Middleware экземпляр исключения и хотите передать его на клиента, указывайте для него параметр cause только если вы уверены, что он содержит только исключения, известные клиенту.
Упаковку объектов исключений в RemoteException перед передачей на клиентский уровень выполняет перехватчик вызовов сервисов - класс ServiceInterceptor. Кроме того, он же выполняет логирование исключений. По умолчанию в журнал выводится вся информация об исключении, включая полный stack trace. Если это нежелательно, можно добавить классу исключения аннотацию @Logging, указав в ней тип логирования:
Например:
@SupportedByClient
@Logging(Logging.Type.BRIEF)
public class FinancialTransactionException extends Exception {
...
5.2.12.3. Обработчики исключений клиентского уровня
Необработанные исключения в блоках Web Client и Desktop Client, возникшие на клиентском уровне или переданные с Middleware, попадают в специальный механизм обработчиков. Этот механизм реализован в модуле gui и доступен обоим клиентам.
Собственные классы обработчиков можно создавать в корневом пакете модуля gui, web или desktop проекта. Обработчик должен быть управляемым бином, реализовывать интерфейс GenericExceptionHandler, в методе handle() которого производить обработку и возвращать true, либо сразу возвращать false, если данный обработчик не может обработать переданное ему исключение. Такое поведение позволяет организовать "цепочку ответственности" обработчиков.
Рекомендуется наследовать классы своих обработчиков от базового класса AbstractGenericExceptionHandler, который умеет разбирать цепочку исключений (с учетом упакованных внутри RemoteException) и реагировать на конкретные типы исключений. Типы исключений, для которых предназначен данный обработчик, указываются в массиве строк, передаваемом в конструкторе обработчика базовому конструктору. Каждая строка массива должна содержать одно полное имя класса обрабатываемого исключения, например:
@Component("sample_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractGenericExceptionHandler {
public ZeroBalanceExceptionHandler() {
super(ZeroBalanceException.class.getName());
}
...
Если класс исключения недоступен на клиенте, следует указывать его имя строковым литералом:
@Component("sample_ForeignKeyViolationExceptionHandler")
public class ForeignKeyViolationExceptionHandler extends AbstractGenericExceptionHandler {
public ForeignKeyViolationExceptionHandler() {
super("java.sql.SQLIntegrityConstraintViolationException");
}
...
В случае использования в качестве базового класса AbstractGenericExceptionHandler логика обработки располагается в методе doHandle(), и может выглядеть следующим образом:
@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, WindowManager windowManager) {
String msg = messages.getMainMessage("zeroBalance.message");
windowManager.showNotification(msg, IFrame.NotificationType.ERROR);
}
Если имени класса исключения недостаточно для того, чтобы принять решение о применимости данного обработчика к исключению, следует определить метод canHandle(), получающий кроме прочего текст исключения. Метод должен вернуть true, если данный обработчик применим для исключения. Например:
@Component("sample_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractGenericExceptionHandler {
public ZeroBalanceExceptionHandler() {
super(ZeroBalanceException.class.getName());
}
@Override
protected boolean canHandle(String className, String message, @Nullable Throwable throwable) {
return StringUtils.containsIgnoreCase(message, "Insufficient or zero funds in your account");
}
...
WindowManager предоставляет метод showExceptionDialog() для отображения диалогового окна с выброшенным исключением и его stacktrace. Метод принимает следующие параметры:
Пример вызова диалога в обработчике исключения:
@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, WindowManager windowManager) {
if (throwable != null)
windowManager.showExceptionDialog(throwable, "Exception is thrown", message);
else
windowManager.showNotification(message, IFrame.NotificationType.ERROR);
}
Для локализации сообщений об ошибках нужно переопределить ключи для соответствующего обработчика в главном пакете сообщений. Ниже приведён пример ключей для RowLevelSecurityExceptionHandler, обрабатывающего нарушения ограничений прав доступа (row-level security):
Bean Validation - опциональный механизм, обеспечивающий единообразную валидацию данных на среднем слое, в Generic UI и в REST API. Он основан на спецификации JSR 349 - Bean Validation 1.1 и ее референсной имплементации: Hibernate Validator.
Ограничения bean validation задаются с помощью аннотаций пакета javax.validation.constraints или собственных аннотаций. Аннотации указываются на декларации класса сущности или POJO, на поле или getter-методе, а также на методе сервиса middleware.
Пример использования стандартных аннотаций валидации на полях сущности:
@Table(name = "DEMO_CUSTOMER")
@Entity(name = "demo$Customer")
public class Customer extends StandardEntity {
@Size(min = 3) // length of value must be longer then 3 characters
@Column(name = "NAME", nullable = false)
protected String name;
@Min(1) // minimum value
@Max(5) // maximum value
@Column(name = "GRADE", nullable = false)
protected Integer grade;
@Pattern(regexp = "\\S+@\\S+") // value must conform to the pattern
@Column(name = "EMAIL")
protected String email;
//...
}
Пример использования собственной аннотации уровня класса (см. ниже):
@CheckTaskFeasibility(groups = {Default.class, UiCrossFieldChecks.class}) // custom validation annotation
@Table(name = "DEMO_TASK")
@Entity(name = "demo$Task")
public class Task extends StandardEntity {
//...
}
Пример валидации параметров и возвращаемого значения метода сервиса:
public interface TaskService {
String NAME = "demo_TaskService";
@Validated // indicates that the method should be validated
@NotNull
String completeTask(@Size(min = 5) String comment, @Valid @NotNull Task task);
}
Аннотация @Valid может быть использована для каскадной валидации параметров метода. В примере выше все ограничения, заданные для объекта Task, также будут валидированы.
Группы ограничений позволяют применять подмножество всех заданных ограничений в зависисмости от логики приложения. Например, вы можете заставить пользователя ввести значение некоторого атрибута сущности в UI, и а то же время иметь возможность установить данный атрибут в null в некотором внутреннем механизме. Для этого необходимо указать атрибут groups в аннотации ограничения, и оно будет действовать только когда эта же группа передается в механизм валидации.
Платформа передает в механизм валидации следующие группы ограничений:
Ограничения могут иметь сообщения для отображения пользователям.
Сообщения могут быть указаны непосредственно в аннотациях валидации, например:
@Pattern(regexp = "\\S+@\\S+", message = "Invalid format")
@Column(name = "EMAIL")
protected String email;
Сообщения можно также поместить в пакет локализованных сообщений и использовать следующий формат указания сообщения в аннотации: {msg://message_pack/message_key} или, в сокращённом виде, {msg://message_key} (только для сущностей). Например:
@Pattern(regexp = "\\S+@\\S+", message = "{msg://com.company.demo.entity/Customer.email.validationMsg}")
@Column(name = "EMAIL")
protected String email;
или, если ограничение задано для сущности и сообщение находится в пакете сообщений данной сущности:
@Pattern(regexp = "\\S+@\\S+", message = "{msg://Customer.email.validationMsg}")
@Column(name = "EMAIL")
protected String email;
Сообщения могут содержать параметры и выражения. Параметры заключаются в фигурные скобки {} и представляют собой либо указатели на локализованные сообщения (см. выше) или параметры аннотации, например {min}, {max}, {value}. Выражения заключаются в фигурные скобки со знаком доллара ${} и могут включать валидируемое значение в виде переменной validatedValue, параметры аннотации типа value или min, и выражения JSR-341 (EL 3.0). Например:
@Pattern(regexp = "\\S+@\\S+", message = "Invalid email: ${validatedValue}, pattern: {regexp}")
@Column(name = "EMAIL")
protected String email;
Значения локализованных сообщений также могут содержать параметры и выражения.
В проекте можно создать собственные ограничения с программной или декларативной валидацией.
Для создания ограничения с программной валидацией выполняет следующее:
@Target({ ElementType.TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = TaskFeasibilityValidator.class)
public @interface CheckTaskFeasibility {
String message() default "{msg://com.company.demo.entity/CheckTaskFeasibility.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class TaskFeasibilityValidator implements ConstraintValidator<CheckTaskFeasibility, Task> {
@Override
public void initialize(CheckTaskFeasibility constraintAnnotation) {
}
@Override
public boolean isValid(Task value, ConstraintValidatorContext context) {
Date now = AppBeans.get(TimeSource.class).currentTimestamp();
return !(value.getDueDate().before(DateUtils.addDays(now, 3)) && value.getProgress() < 90);
}
}
@CheckTaskFeasibility(groups = UiCrossFieldChecks.class)
@Table(name = "DEMO_TASK")
@Entity(name = "demo$Task")
public class Task extends StandardEntity {
@Future
@Temporal(TemporalType.DATE)
@Column(name = "DUE_DATE")
protected Date dueDate;
@Min(0)
@Max(100)
@Column(name = "PROGRESS", nullable = false)
protected Integer progress;
//...
}
Собственные аннотации могут также быть созданы как композиции имеющихся, например:
@NotNull
@Size(min = 2, max = 14)
@Pattern(regexp = "\\d+")
@Target({METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidProductCode {
String message() default "{msg://om.company.demo.entity/ValidProductCode.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
При использовании композитных ограничений результирующий набор нарушений ConstraintViolation будет содержать отдельные записи для каждого включенного ограничения. Для того, чтобы получить одну запись нарушения, добавьте @ReportAsSingleViolation классу вашей аннотации.
Для каскадной валидации связанных объектов на ссылочные поля необходимо добавить аннотацию @Valid:
public class Cart {
@Size(min = 1)
@Valid
private List<Product> products;
// ...
}
В примере выше, при валидации экземпляра Cart список продуктов будет проверен на наличие хотя-бы одной записи, и все экземпляры Product в списке будут также провалидированы.
Аннотации валидации, заданные в CUBA
Кроме стандартных аннотаций из пакета javax.validation.constraints можно использовать следующую аннотацию, определенную в платформе:
public interface MyService {
String NAME = "sample_MyService";
@Validated
void processFoo(@RequiredView("foo-view") Foo foo);
@Validated
void processFooList(@RequiredView("foo-view") List<Foo> fooList);
@Validated
@RequiredView("bar-view")
Bar loadBar(@RequiredView("foo-view") Foo foo);
}
Компоненты Generic UI, соединенные с источником данных, получают экземпляр BeanValidator для проверки значения. Валидатор вызывается из метода Component.Validatable.validate(), реализуемого компонентом, и может выбрасывать исключение CompositeValidationException, содержащее набор объектов нарушений.
Стандартный валидатор может быть программно удален или проинициализирован другой группой ограничений:
public class Screen extends AbstractWindow {
@Inject
private TextField field1;
@Inject
private TextField field2;
public void init(Map<String, Object> params) {
// Completely remove bean validation from the UI component
field1.getValidators().stream()
.filter(validator -> validator instanceof BeanValidator)
.forEach(textField::removeValidator);
// Here validators will check only constraints with explicitly set UiComponentChecks group, because
// the Default group will not be passed
field2.getValidators().stream()
.filter(validator -> validator instanceof BeanValidator)
.forEach(validator -> {
((BeanValidator) validator).setValidationGroups(new Class[] {UiComponentChecks.class});
});
}
}
По умолчанию, BeanValidator содержит две группы: Default и UiComponentChecks.
Если атрибут сущности аннотирован @NotNull без группы ограничений, он будет помечен как обязательный в метаданных, и UI-компоненты работающие с данным атрибутом через источник данных, будут иметь свойство required = true.
Компоненты DateField и DatePicker автоматически устанавливают свои свойства rangeStart и rangeEnd в соответствии с аннотациями @Past и @Future, не учитывая текущее время.
Экраны редактирования выполняют валидацию ограничений уровня класса при коммите, если ограничения включают группу UiCrossFieldChecks и все проверки ограничений уровня атрибутов прошли успешно. Валидацию данного типа можно отключить с помощью свойства экрана crossFieldValidate в XML-дескрипторе или в контроллере:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editorCaption"
class="com.company.demo.web.task.TaskEdit"
datasource="taskDs"
crossFieldValidate="false">
<!-- ... -->
</window>
public class TaskEdit extends AbstractEditor<Task> {
@Override
public void init(Map<String, Object> params) {
setCrossFieldValidate(false);
}
}
Сервисы среднего слоя выполняют валидацию параметров и результатов методов, если метод имеет аннотацию @Validated в интерфейсе сервиса. Например:
public interface TaskService {
String NAME = "demo_TaskService";
@Validated
@NotNull
String completeTask(@Size(min = 5) String comment, @NotNull Task task);
}
Аннотация @Validated может содержать указание групп ограничений для применения подмножества имеющихся ограничений. Если группы не указаны, то по умолчанию используются следующие:
При возникновении ошибок валидации выбрасываются исключения MethodParametersValidationException и MethodResultValidationException.
При выполнении некоторой специфической программной валидации в сервисе, можно использовать исключение CustomValidationException для того, чтобы клиенты получали информацию об ошибках в том же формате что и от стандартной валидации. Это может быть особенно актуально для клиентов REST API.
Универсальный REST API автоматически выполняет bean validation для действий создания и изменения сущностей. Ошибки валидации передаются клиенту следующим образом:
[
{
"message": "Invalid email: aaa",
"messageTemplate": "{msg://com.company.demo.entity/Customer.email.validationMsg}",
"path": "email",
"invalidValue": "aaa"
}
]
Bean validation можно запустить программно используя интерфейс инфраструктуры BeanValidation, доступный и на middleware и на клиентском уровне. Через данный интерфейс необходимо получить реализацию javax.validation.Validator, которая и запускает валидацию. Результатом валидации является набор объектов типа ConstraintViolation. Например:
@Inject
private BeanValidation beanValidation;
public void save(Foo foo) {
Validator validator = beanValidation.getValidator();
Set<ConstraintViolation<Foo>> violations = validator.validate(foo);
// ...
}
5.2.14. Контроль доступа к атрибутам сущностей
Подсистема безопасности позволяет управлять доступом к атрибутам сущностей в соответствии с правами пользователя. Т.е. фреймворк может сделать атрибут read-only или скрыть его в зависимости от набора ролей, назначенных текущему пользователю. Однако, иногда может потребоваться изменять доступ к атрибуту в зависимости также от текущего состояния экземпляра сущности или связанных сущностей.
Механизм контроля доступа к атрибутам позволяет создавать правила того, какие атрибуты должны быть скрыты, нередактируемы или обязательны к заполнению для некоторого экземпляра сущности, и применять эти правила к компонентам Generic UI и в REST API.
Данный механизм работает следующим образом:
Для создания правил для некоторого типа сущностей, необходимо выполнить следующее:
Рассмотрим пример сущности Order, имеющей атрибуты customer и amount. Для ограничения доступа к атрибуту amount в зависимости от customer можно создать следующее правило:
package com.company.sample.core;
import com.company.sample.entity.Order;
import com.haulmont.cuba.core.app.SetupAttributeAccessHandler;
import com.haulmont.cuba.core.app.events.SetupAttributeAccessEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component("sample_OrderAttributeAccessHandler")
public class OrderAttributeAccessHandler implements SetupAttributeAccessHandler<Order> {
@Override
public boolean supports(Class clazz) {
return Order.class.isAssignableFrom(clazz);
}
@Override
public void setupAccess(SetupAttributeAccessEvent<Order> event) {
Order order = event.getEntity();
if (order.getCustomer() != null) {
if ("PLATINUM".equals(order.getCustomer().getGrade().getCode())) {
event.addHidden("amount");
} else if ("GOLD".equals(order.getCustomer().getGrade().getCode())) {
event.addReadOnly("amount");
}
}
}
}
Контроль доступа к атрибутам в Generic UI
Фреймворк автоматически применяет ограничения доступа к экрану перед вызовом метода ready() его контроллера. Если вы не хотите этого для некоторого экрана, переопределите метод isAttributeAccessControlEnabled() его контроллера и верните из него false.
Пересчитать и применить ограничения к экрану можно и когда он уже открыт, в ответ на действия пользователя. Для этого необходимо использовать бин AttributeAccessSupport, передавая ему текущий экран и сущность, состояние которой изменилось. Например:
package com.company.sample.web.order;
import com.company.sample.entity.Order;
import com.haulmont.cuba.gui.AttributeAccessSupport;
import com.haulmont.cuba.gui.components.AbstractEditor;
import com.haulmont.cuba.gui.data.Datasource;
import javax.inject.Inject;
import java.util.Map;
public class OrderEdit extends AbstractEditor<Order> {
@Inject
private Datasource<Order> orderDs;
@Inject
private AttributeAccessSupport attributeAccessSupport;
@Override
public void init(Map<String, Object> params) {
orderDs.addItemPropertyChangeListener(e -> {
if ("customer".equals(e.getProperty())) {
attributeAccessSupport.applyAttributeAccess(this, true, getItem());
}
});
}
}
Второй параметр метода applyAttributeAccess() - булевское значение, которое указывает, нужно ли сбрасывать ограничения доступа к компонентам в дефолтные настройки перед тем, как применить новые. Если передано true, возможные программные изменения в этих настройках будут потеряны. Когда данный метод вызывается автоматически перед открытием окна, передается false. Когда же вы вызываете данный метод в ответ на UI-события, передавайте true, иначе ограничения компонентов будут суммироваться, а не заменяться.
|
Ограничения доступа к атрибутам применяются только к компонентам, связанным с одним атрибутом сущности, например TextField или LookupField. Table и другие компоненты, реализующие интерфейс ListComponent, не затрагиваются. Поэтому если вы пишете правило, скрывающее атрибут для некоторых экземпляров, рекомендуется не показывать этот атрибут в таблицах совсем. |
5.3. Компоненты работы с базой данных
В данном разделе приведена информация о возможных типах СУБД приложений на платформе CUBA. Кроме того, описан механизм на основе скриптов, с помощью которого можно создать новую базу данных, и в дальнейшем поддерживать ее в актуальном состоянии на протяжении всего цикла разработки и эксплуатации приложения.
Компоненты работы с базой данных принадлежат блоку Middleware, другие блоки приложения не имеют прямого доступа к БД.
Дополнительная практическая информация по работе с базой данных приведена в секции ниже.
Тип используемой СУБД определяется свойствами приложения cuba.dbmsType и (опционально) cuba.dbmsVersion, которые влияют на поведение механизмов, зависящих от типа базы данных.
Приложение обращается к БД через источник данных javax.sql.DataSource, который извлекается из JNDI по имени, заданному в свойстве приложения cuba.dataSourceJndiName (по умолчанию java:comp/env/jdbc/CubaDS). Конфигурация источника данных в случае стандартного развертывания задается в файле context.xml модуля core. Источник данных должен использовать JDBC-драйвер, соответствующий выбранной СУБД.
Платформа "из коробки" поддерживает следующие СУБД:
|
cuba.dbmsType |
cuba.dbmsVersion |
JDBC driver | |
|
HSQLDB |
hsql |
org.hsqldb.jdbc.JDBCDriver | |
|
PostgreSQL 8.4+ |
postgres |
org.postgresql.Driver | |
|
Microsoft SQL Server 2005 |
mssql |
2005 |
net.sourceforge.jtds.jdbc.Driver |
|
Microsoft SQL Server 2008 |
mssql |
com.microsoft.sqlserver.jdbc.SQLServerDriver | |
|
Microsoft SQL Server 2012+ |
mssql |
2012 |
com.microsoft.sqlserver.jdbc.SQLServerDriver |
|
Oracle Database 11g+ |
oracle |
oracle.jdbc.OracleDriver | |
|
MySQL 5.6+ |
mysql |
com.mysql.jdbc.Driver |
Таблица ниже описывает рекомендованное соответствие типов данных между атрибутами сущностей в Java и колонками таблиц различных СУБД. Эти типы автоматически выбираются Studio при генерации скриптов создания и обновления БД, и для них гарантируется работоспособность всех механизмов платформы.
|
Java |
HSQL |
PostgreSQL |
MS SQL Server |
Oracle |
MySQL |
|
UUID |
varchar(36) |
uuid |
uniqueidentifier |
varchar2(32) |
varchar(32) |
|
Date |
timestamp |
timestamp |
datetime |
timestamp |
datetime(3) |
|
java.sql.Date |
timestamp |
date |
datetime |
date |
date |
|
java.sql.Time |
timestamp |
time |
datetime |
timestamp |
time(3) |
|
BigDecimal |
decimal(p, s) |
decimal(p, s) |
decimal(p, s) |
number(p, s) |
decimal(p, s) |
|
Double |
double precision |
double precision |
double precision |
float |
double precision |
|
Long |
bigint |
bigint |
bigint |
number(19) |
bigint |
|
Integer |
integer |
integer |
integer |
integer |
integer |
|
Boolean |
boolean |
boolean |
tinyint |
char(1) |
boolean |
|
String (limited) |
varchar(n) |
varchar(n) |
varchar(n) |
varchar2(n) |
varchar(n) |
|
String (unlimited) |
longvarchar |
text |
varchar(max) |
clob |
longtext |
|
byte[] |
longvarbinary |
bytea |
image |
blob |
longblob |
Как правило, всю работу по преобразованию данных между БД и кодом Java выполняет слой ORM совместно с соответствующим JDBC драйвером. Это означает, что при работе с данными через методы EntityManager и запросы на JPQL никакой ручной конвертации выполнять не нужно - вы просто используете типы Java, перечисленные в левой колонке таблицы.
При использовании native SQL через EntityManager.createNativeQuery() или через QueryRunner для разных типов СУБД некоторые типы данных в Java коде будут отличаться от приведенных. В первую очередь это касается атрибутов типа UUID - только драйвер PostgreSQL возвращает значения соответствующих колонок в этом типе, для других серверов это будет String. Для обеспечения независимости кода от используемой СУБД рекомендуется конвертировать типы параметров и результатов запросов с помощью интерфейса DbTypeConverter.
5.3.1.1. Поддержка произвольных СУБД
На уровне прикладного проекта можно реализовать работу с любой СУБД, поддерживаемой фреймворком ORM (EclipseLink). Для этого достаточно выполнить следующее:
task createDb(dependsOn: assemble, type: CubaDbCreation) {
dbms = 'my' // DBMS code
driver = 'net.my.jdbc.Driver' // JDBC driver class
dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb' // Database URL
masterUrl = 'jdbc:my:myserver://192.168.47.45/master' // URL of a master DB to connect to for creating the application DB
dropDbSql = 'drop database mydb;' // Drop database statement
createDbSql = 'create database mydb;' // Create database statement
timeStampType = 'datetime' // Date and time datatype - needed for SYS_DB_CHANGELOG table creation
dbUser = 'sa'
dbPassword = 'saPass1'
}
task updateDb(dependsOn: assemble, type: CubaDbUpdate) {
dbms = 'my' // DBMS code
driver = 'net.my.jdbc.Driver' // JDBC driver class
dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb' // Database URL
dbUser = 'sa'
dbPassword = 'saPass1'
}
В дополнение к свойству приложения cuba.dbmsType существует опциональное свойство cuba.dbmsVersion. Оно влияет на выбор имплементаций интерфейсов DbmsFeatures, SequenceSupport, DbTypeConverter, и на поиск скриптов создания и обновления БД.
Имя класса имплементации интеграционного интерфейса формируется следующим образом: TypeVersionName. Здесь Type - значение cuba.dbmsType с заглавной буквы, Version - значение cuba.dbmsVersion, Name - имя интерфейса. Пакет класса должен быть таким же, как у интерфейса. Если класс с таким именем отсутствует, предпринимается попытка найти класс с именем без версии: TypeName. Если и такого класса нет, выдается исключение.
Например, в платформе определен класс com.haulmont.cuba.core.sys.persistence.Mssql2012SequenceSupport, который вступит в силу, если в проекте указаны следующие свойства:
cuba.dbmsType = mssql
cuba.dbmsVersion = 2012
При поиске скриптов создания и обновления БД каталог с именем type-version имеет приоритет над каталогом с именем type. Это значит, что скрипты каталога type-version заменяют одноименные скрипты каталога type. В каталоге type-version могут быть и скрипты с собственными именами, они будут также добавлены в общий набор скриптов для выполнения. Сортировка скриптов производится по пути начиная с каталога, вложенного в type или type-version, то есть без учета того, в каком каталоге (с версией или без) находится скрипт.
Например, следующим образом можно определить скрипты создания БД для Microsoft SQL Server для версий ниже и выше 2012:
modules/core/db/init/
mssql/
10.create-db.sql
20.create-db.sql
30.create-db.sql
mssql-2012/
10.create-db.sql
5.3.2. Скрипты создания и обновления БД
Проект CUBA-приложения всегда содержит два набора скриптов (см. также раздел Создание схемы БД):
При изменении модели данных необходимо отразить соответствующее изменение схемы БД и в скриптах создания, и в скриптах обновления. Например, при добавлении атрибута address в сущность Customer, нужно:
create table SALES_CUSTOMER (
ID varchar(36) not null,
CREATE_TS timestamp,
CREATED_BY varchar(50),
NAME varchar(100),
ADDRESS varchar(200), -- added column
primary key (ID)
)
alter table SALES_CUSTOMER add ADDRESS varchar(200)
|
Обратите внимание, что генератор скриптов Studio не отслеживает изменения Column definition и sqlType специализированных datatype. Таким образом, при их изменении соответствующие скрипты обновления необходимо создавать вручную. |
Скрипты создания располагаются в каталоге /db/init модуля core. Для каждого типа СУБД, поддерживаемой приложением, создается свой набор скриптов и располагается в подкаталоге с именем, соответствующим свойству приложения cuba.dbmsType, например, /db/init/postgres. Имена скриптов создания должны иметь вид {optional_prefix}create-db.sql.
Скрипты обновления располагаются в каталоге /db/update модуля core. Для каждого типа СУБД, поддерживаемой приложением, создается свой набор скриптов и располагается в подкаталоге с именем, соответствующим свойству приложения cuba.dbmsType, например, /db/update/postgres.
Скрипты обновления могут быть двух типов: с расширением *.sql или с расширением *.groovy. SQL-скрипты являются основным средством обновления базы данных. Groovy-скрипты выполняются только механизмом запуска скриптов БД сервером, поэтому применяются в основном на этапе эксплуатации приложения - как правило, это процессы миграции или импорта данных, которые невозможно реализовать на SQL. Чтобы пропустить скрипты Groovy при обновлении, вы можете выполнить следующую команду из командной строки:
delete from sys_db_changelog where script_name like '%groovy' and create_ts > (now() - interval '1 hour')
Скрипты обновления должны иметь имена, которые при сортировке в алфавитном порядке образуют правильную последовательность их выполнения (обычно это хронологическая последовательность их создания). Поэтому при ручном создании рекомендуется задавать имя скрипта обновления в виде {yymmdd}-{description}.sql, где yy - год, mm - месяц, dd - день, description - краткое описание скрипта. Например, 121003-addCodeToCategoryAttribute.sql. Studio при автоматической генерации скриптов также придерживается этого формата.
Чтобы groovy-скрипты также выполнялись командой updateDb, они обязательно должны иметь расширение .upgrade.groovy и ту же логику именования, что и sql-скрипты. Действия из набора Post update в этих скриптах не поддерживаются, для привязки к данным используются стандартные переменные ds (доступ к источнику данных) и log (доступ к журналу сервера). Выполнение groovy-скриптов можно запретить, прописа executeGroovy = false в команде updateDb в build.gradle.
Скрипты обновления можно группировать в подкаталоги, главное, чтобы путь к скрипту с учетом подкаталога не нарушал хронологической последовательности. Например, можно создавать подкаталоги по номеру года или по году и месяцу.
В развернутом приложении скрипты создания и обновления БД располагаются в специальном каталоге скриптов базы данных, задаваемым свойством приложения cuba.dbDir.
5.3.2.1. Структура SQL-скриптов
SQL-скрипты создания и обновления представляют собой текстовые файлы с набором DDL и DML команд, разделенных символом “^”. Символ “^” применяется для того, чтобы можно было применять разделитель “;” в составе сложных команд, например, при создании функций или триггеров. Механизм исполнения скриптов разделяет входной файл на команды по разделителю “^” и выполняет каждую команду в отдельной транзакции. Это означает, что при необходимости можно сгруппировать несколько простых операторов (например, insert), разделенных точкой с запятой, и обеспечить их выполнение в одной транзакции.
|
Разделитель "^" может быть заэкранирован путем его удвоения. Например, если вам нужно передать значение ^[0-9\s]+$, скрипт должен содержать строку ^^[0-9\s]+$. |
Пример SQL-скрипта обновления:
create table LIBRARY_COUNTRY (
ID varchar(36) not null,
CREATE_TS time,
CREATED_BY varchar(50),
NAME varchar(100) not null,
primary key (ID)
)^
alter table LIBRARY_TOWN add column COUNTRY_ID varchar(36) ^
alter table LIBRARY_TOWN add constraint FK_LIBRARY_TOWN_COUNTRY_ID foreign key (COUNTRY_ID) references LIBRARY_COUNTRY(ID)^
create index IDX_LIBRARY_TOWN_COUNTRY on LIBRARY_TOWN (COUNTRY_ID)^
5.3.2.2. Структура Groovy-скриптов
Groovy-скрипты обновления имеют следующую структуру:
Основная часть предназначена в первую очередь, как и обычные SQL-скрипты, для обновления схемы данных.
В этой части скрипта удобно, например, выполнять импорт данных, так как в ней можно использовать интерфейс Persistence и объекты модели данных.
На вход Groovy-скриптов механизм выполнения передает следующие переменные:
|
Groovy-скрипты выполняются только механизмом запуска скриптов БД сервером. |
Пример Groovy-скрипта обновления:
import com.haulmont.cuba.core.Persistence
import com.haulmont.cuba.core.global.AppBeans
import com.haulmont.refapp.core.entity.Colour
import groovy.sql.Sql
log.info('Executing actions in update phase')
Sql sql = new Sql(ds)
sql.execute """ alter table MY_COLOR add DESCRIPTION varchar(100); """
// Add post update action
postUpdate.add({
log.info('Executing post update action using fully functioning server')
def p = AppBeans.get(Persistence.class)
def tr = p.createTransaction()
try {
def em = p.getEntityManager()
Colour c = new Colour()
c.name = 'yellow'
c.description = 'a description'
em.persist(c)
tr.commit()
} finally {
tr.end()
}
})
5.3.3. Выполнение скриптов БД задачами Gradle
Данный механизм применяется обычно разработчиками приложения для собственного экземпляра базы данных. Выполнение скриптов в этом случае сводится к запуску специальных задач Gradle, описанных в скрипте сборки build.gradle. Это можно сделать как из командной строки, так и с помощью интерфейса Studio.
Для запуска скриптов создания БД служит задача createDb. В Studio ей соответствует команда главного меню Run → Create database. При запуске задачи происходит следующее:
Для запуска скриптов обновления БД служит задача updateDb. В Studio ей соответствует команда главного меню Run → Update database. При запуске задачи происходит следующее:
5.3.4. Выполнение скриптов БД сервером
Механизм выполнения скриптов сервером предназначен для приведения БД в актуальное состояние на старте сервера приложения, и активируется во время инициализации блока Middleware. Понятно, что при этом приложение должно быть собрано и развернуто на сервере, будь то собственный Tomcat разработчика или сервер в режиме эксплуатации.
Данный механизм в зависимости от описанных ниже условий выполняет либо скрипты создания, либо скрипты обновления, то есть он может и инициализировать БД с нуля, и обновлять ее. Однако, в отличие от описанной в предыдущем разделе задачи Gradle createDb, для выполнения инициализации базы она должна существовать - сервер не создает БД автоматически, а только прогоняет на ней скрипты.
Механизм выполнения скриптов сервером действует следующим образом:
Перед выполнением скриптов обновления производится проверка, все ли скрипты создания схемы компонентов приложения выполнены (путем выборки из таблицы SYS_DB_CHANGELOG). Если обнаруживается, что БД не инициализирована для работы некоторого компонента, выполняются его скрипты создания.
Механизм выполнения скриптов на старте сервера включается свойством приложения cuba.automaticDatabaseUpdate.
В запущенном приложении механизм выполнения скриптов можно стартовать с помощью JMX-бина app-core.cuba:type=PersistenceManager, вызвав его метод updateDatabase() с параметром update. Понятно, что таким способом можно только обновить БД, а не проинициализировать новую, так как войти в систему для запуска метода JMX-бина при пустой БД невозможно. При этом следует иметь в виду, что если на старте Middleware или при входе пользователя в систему начнется инициализация той части модели данных, которая уже не соответствует устаревшей схеме БД, то произойдет ошибка, и продолжение работы станет невозможным. Именно поэтому универсальным является только автоматическое обновление БД на старте сервера перед инициализацией модели данных.
JMX-бин app-core.cuba:type=PersistenceManager имеет еще один метод, относящийся к механизму обновления БД: findUpdateDatabaseScripts(). Он возвращает список новых скриптов обновления, имеющихся в каталоге и не зарегистрированных в БД.
Практические рекомендации по использованию механизма обновления БД сервером приведены в Создание и обновление БД при эксплуатации приложения.
На следующем рисунке приведены основные компоненты среднего слоя CUBA-приложения.
Рисунок 13. Компоненты среднего слоя
Services – управляемые контейнером компоненты, формирующие границу приложения и предоставляющие интерфейс клиентскому уровню приложения. Сервисы могут содержать бизнес-логику сами, либо делегировать выполнение Managed Beans.
Managed Beans – управляемые контейнером компоненты, содержащие бизнес-логику приложения. Вызываются сервисами, другими бинами или через опциональный JMX-интерфейс.
Persistence − инфраструктурный интерфейс для доступа к функциональности хранения данных: управлению транзакциями и ORM.
Сервисы образуют слой компонентов, определяющий множество операций Middleware, доступных клиентскому уровню приложения. Другими словами, сервис представляет собой точку входа в бизнес-логику среднего слоя. В сервисе можно управлять транзакциями, проверять права пользователей, работать с базой данных или делегировать выполнение другим управляемым бинам среднего слоя.
Диаграмма классов сервиса, отображающая основные компоненты сервиса:
Интерфейс сервиса располагается в модуле global и доступен на клиентском уровне и на Middleware. Во время выполнения, на клиентском уровне для интерфейса сервиса создается прокси-объект, который обеспечивает вызовы методов бина, реализующего сервис, с помощью механизма Spring HTTP Invoker.
Бин, реализующий сервис, располагается в модуле core и доступен только на среднем слое.
ServiceInterceptor автоматически вызывается для каждого метода сервиса через Spring AOP. Он проверяет наличие в потоке выполнения пользовательской сессии, а также трансформирует и логирует исключения, если сервис вызван с клиентского уровня.
Имена интерфейсов сервисов должны заканчиваться на Service, имена классов реализации на ServiceBean.
При создании сервиса необходимо выполнить следующее:
package com.sample.sales.core;
import com.sample.sales.entity.Order;
public interface OrderService {
String NAME = "sales_OrderService";
void calculateTotals(Order order);
}
package com.sample.sales.core;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Service;
@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {
@Override
public void calculateTotals(Order order) {
}
}
Класс сервиса, как и класс любого другого управляемого бина, должен находиться внутри дерева пакетов с корнем, заданным в элементе context:component-scan файла spring.xml. В нашем случае файл spring.xml содержит элемент:
<context:component-scan base-package="com.sample.sales"/>
что означает, что поиск аннотированных бинов для данного блока приложения будет происходить начиная с пакета com.sample.sales.
Если некоторую бизнес-логику требуется вызывать из разных сервисов либо других компонентов Middleware, ее необходимо выделить и инкапсулировать внутри соответствующего Managed Bean. Например:
// service interface
public interface SalesService {
String NAME = "sample_SalesService";
BigDecimal calculateSales(UUID customerId);
}
// service implementation
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {
@Inject
private SalesCalculator salesCalculator;
@Transactional
@Override
public BigDecimal calculateSales(UUID customerId) {
return salesCalculator.calculateSales(customerId);
}
}
// managed bean encapsulating business logic
@Component
public class SalesCalculator {
@Inject
private Persistence persistence;
public BigDecimal calculateSales(UUID customerId) {
Query query = persistence.getEntityManager().createQuery(
"select sum(o.amount) from sample$Order o where o.customer.id = :customerId");
query.setParameter("customerId", customerId);
return (BigDecimal) query.getFirstResult();
}
}
5.4.1.2. Использование сервиса
Для того чтобы вызывать сервис, в клиентском блоке приложения для него должен быть создан соответствующий прокси-объект. Делается это путем объявления имени и интерфейса сервиса в параметрах фабрики прокси-объектов. Для блока Web Client это бин класса WebRemoteProxyBeanCreator, для Web Portal - PortalRemoteProxyBeanCreator, для Desktop Client - RemoteProxyBeanCreator .
Фабрика прокси-объектов конфигурируется в файле spring.xml соответствующего клиентского блока.
Например, чтобы в приложении sales вызвать с веб-клиента сервис sales_OrderService, необходимо добавить в файл web-spring.xml модуля web следующее:
<bean id="sales_proxyCreator" class="com.haulmont.cuba.web.sys.remoting.WebRemoteProxyBeanCreator">
<property name="serverSelector" ref="cuba_ServerSelector"/>
<property name="remoteServices">
<map>
<entry key="sales_OrderService" value="com.sample.sales.core.OrderService"/>
</map>
</property>
</bean>
Все импортируемые сервисы объявляются в одном свойстве remoteServices в элементах map/entry.
|
CUBA Studio автоматически регистрирует сервисы во всех клиентских блоках приложения. |
С точки зрения прикладного кода прокси-объект сервиса на клиентском уровне является обычным бином Spring и может быть получен либо инжекцией, либо с помощью класса AppBeans, например:
@Inject
private OrderService orderService;
public void calculateTotals() {
orderService.calculateTotals(order);
}
DataService является фасадом для вызова серверной реализации DataManager с клиентского уровня. DataService не рекомендуется использовать в прикладном коде. Вместо него и на клиентском уровне, и на Middleware следует использовать DataManager.
Предпочтительный способ работы с данными в CUBA-приложениях - использование сущностей: либо декларативно в источниках данных и связанных с данными компонентах, либо программно через DataManager или EntityManager. Сущности отображаются на данные в хранилище, которое обычно представляет собой реляционную БД. Приложение может работать с несколькими хранилищами, так что его модель данных будет содержать сущности, отображаемые на данные из разных БД.
Некоторая сущность может принадлежать только одному хранилищу. Сущности из разных хранилищ можно отображать на одном экране UI, при этом DataManager обеспечивает их чтение и запись в соответствующее хранилище. В зависимости от типа сущности, DataManager выбирает зарегистрированное хранилище, представленное реализацией интерфейса DataStore, и делегирует ему загрузку или сохранение. При управлении транзакциями и работе с сущностями через EntityManager необходимо явно указывать, какое хранилище использовать. Подробнее см. методы интерфейса Persistence и параметры аннотации @Transactional.
Платформа содержит единственную реализацию интерфейса DataStore: RdbmsStore, предназначенную для работы с реляционными СУБД через слой ORM. Кроме того, в проекте приложения можно реализовать DataStore для интеграции, например, с нереляционной СУБД или внешней системой, имеющей REST интерфейс.
Каждое CUBA-приложение имеет основное хранилище, которое содержит системные сущности и используется для входа пользователей в приложение. В данном руководстве под базой данных всегда имеется в виду основное хранилище, если явно не оговорено другое. Основное хранилище должно представлять собой реляционную БД, подключенную через источник данных JDBC. Источник данных основного хранилища находится в JNDI с именем, указанным в свойстве приложения cuba.dataSourceJndiName (по умолчанию jdbc/CubaDS).
Имена дополнительных хранилищ указываются в свойстве приложения cuba.additionalStores. Если дополнительное хранилище является реляционной БД (RdbmsStore), необходимо указать для него следующие свойства приложения:
Если вы реализовали интерфейс DataStore в проекте, укажите имя бина реализации в свойстве приложения cuba.storeImpl_{store_name}.
Предположим, что в вашем проекте два дополнительных хранилища: db1 (база данных PostgreSQL) and mem1 (некоторое in-memory хранилище, реализованное бином проекта). Тогда необходимо указать следующие свойства приложения в файле app.properties модуля core:
cuba.additionalStores = db1, mem1
cuba.dataSourceJndiName_db1 = jdbc/db1
cuba.dbmsType_db1 = postgres
cuba.persistenceConfig_db1 = com/company/sample/db1-persistence.xml
cuba.storeImpl_mem1 = sample_InMemoryStore
Свойства cuba.additionalStores и cuba.persistenceConfig_db1 необходимо также указать в файлах свойств всех используемых блоков приложения (web-app.properties, portal-app.properties, и т.д.).
|
CUBA Studio позволяет настраивать дополнительные хранилища на вкладке Project properties > Advanced. Studio автоматически создает все необходимые свойства приложения и поддерживает соответствующие файлы persistence.xml. После этого хранилище можно будет указать в поле Data store редактора сущности. Кроме того, хранилище можно будет выбрать при запуске мастера Generate model для создания сущностей, отображенных на существующую схему БД. |
Ссылки между сущностями из разных хранилищ
DataManager может автоматически поддерживать TO-ONE ссылки между сущностями из разных хранилищ, если они объявлены нужным образом. Например, рассмотрим случай, когда необходимо в сущности Order, находящейся в главном хранилище, иметь ссылку на сущность Customer из дополнительного хранилища. Необходимо сделать следующее:
@SystemLevel
@Column(name = "CUSTOMER_ID")
private Long customerId;
@Transient
@MetaProperty(related = "customerId")
private Customer customer;
После этого, когда Order будет загружаться с представлением, включающим атрибут customer, DataManager будет автоматически загружать связанные экземпляры Customer из дополнительного хранилища. Загрузка коллекций оптимизирована по производительности: после загрузки списка заказов загрузка покупателей из доп. хранилища производится пакетами. Размер пакета определяется свойством приложения cuba.crossDataStoreReferenceLoadingBatchSize.
При коммите графа объектов, включающего Order со ссылкой на Customer, DataManager сохранит сущности через соответствующие имплементации DataStore, а затем сохранит идентификатор Customer в атрибуте customerId сущности Order.
Ссылки между сущностями из разных хранилищ поддерживаются компонентом Filter.
|
CUBA Studio автоматически поддерживает набор атрибутов для ссылок между сущностями из разных хранилищ, если в качестве ассоциации выбирается сущность из другого хранилища. |
Интерфейс Persistence является точкой входа в функциональность хранения данных, предоставляемую слоем ORM.
Методы интерфейса:
|
Для всех объектов javax.sql.Connection, получаемых методом getDataSource().getConnection(), необходимо после использования соединения вызвать метод close() в секции finally. В противном случае соединение не вернется в пул, через какое-то время пул переполнится, и приложение не сможет выполнять запросы к базе данных. |
ManagedBean, содержащий вспомогательные методы работы с хранилищем данных. Интерфейс PersistenceTools можно получить либо методом Persistence.getTools(), либо как любой другой бин - инжекцией или через класс AppBeans.
Методы PersistenceTools:
Данный метод работает только для экземпляров в состоянии Managed.
Предположим, в персистентный контекст загружен экземпляр Order, и нужно получить значение идентификатора экземпляра Customer, связанного с данным Заказом. Стандартное решение order.getCustomer().getId() приведет к выполнению SQL запроса к БД для загрузки экземпляра Customer, что в данном случае избыточно, так как значение идентификатора Покупателя физически находится также и в таблице Заказов. Выполнение же
persistence.getTools().getReferenceId(order, "customer")
не вызовет никаких дополнительных запросов к базе данных.
Данный метод работает только для экземпляров в состоянии Managed.
Для расширения набора вспомогательных методов в конкретном приложении бин PersistenceTools можно переопределить. Примеры работы с расширенным интерфейсом:
MyPersistenceTools tools = persistence.getTools();
tools.foo();
((MyPersistenceTools) persistence.getTools()).foo();
Интерфейс, определяющий методы для конвертации данных между значениями атрибутов модели данных и параметрами и результатами запросов JDBC. Объект данного интерфейса можно получить методом Persistence.getDbTypeConverter().
Методы DbTypeConverter:
Object-Relational Mapping - объектно-реляционное отображение - технология связывания таблиц реляционной базы данных с объектами языка программирования. В платформе CUBA используется реализация ORM основе фреймворка EclipseLink.
Использование ORM дает ряд очевидных преимуществ:
В то же время, имеются и некоторые недостатки. Во-первых, разработчик, использующий ORM непосредственно, должен хорошо понимать особенности работы этой технологии. Во-вторых, усложняется оптимизация SQL и использование особенности применяемой СУБД.
|
Если вы столкнулись с проблемами производительности при доступе к БД, начните с того, что проверьте, какие конкретно SQL-операторы выполняются в вашем приложении. Вы можете использовать логгер eclipselink.sql для вывода всех SQL-операторов, генерируемых ORM, в файл лога. |
EntityManager - основной интерфейс ORM, служит для управления персистентными сущностями.
|
В разделе DataManager vs. EntityManager приведена информация о различиях между EntityManager и DataManager. |
Ссылку на EntityManager можно получить через интерфейс Persistence, вызовом метода getEntityManager(). Полученный экземпляр EntityManager привязан к текущей транзакции, то есть все вызовы getEntityManager() в рамках одной транзакции возвращают один и тот же экземпляр EntityManager. После завершения транзакции обращения к данному экземпляру невозможны.
Экземпляр EntityManager содержит в себе персистентный контекст – набор экземпляров сущностей, загруженных из БД или только что созданных. Персистентный контекст является своего рода кэшем данных в рамках транзакции.EntityManager автоматически сбрасывает в БД все изменения, сделанные в его персистентном контексте, в момент коммита транзакции, либо при явном вызове метода flush().
Интерфейс EntityManager, используемый в CUBA-приложениях, в основном повторяет стандартный javax.persistence.EntityManager. Рассмотрим его основные методы:
Если переданный экземпляр находится в Detached состоянии, сначала выполняется merge().
При формировании запроса к БД учитывается представление, переданное в параметре данного метода. В результате в персистентном контексте окажется граф объектов, для которого загружены все атрибуты представления. Если не передано никакого представления, по умолчанию будет использовано представление _local.
|
В отличие от DataManager, все локальные атрибуты сущностей загружаются независимо от того, указаны ли они в представлении или нет. В EntityManager представление влияет только на загрузку атрибутов-ссылок. |
Пример использования EntityManager в сервисе:
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {
@Inject
private Persistence persistence;
@Override
public BigDecimal calculateSales(UUID customerId) {
BigDecimal result;
// start transaction
try (Transaction tx = persistence.createTransaction()) {
// get EntityManager for the current transaction
EntityManager em = persistence.getEntityManager();
// create and execute Query
Query query = em.createQuery(
"select sum(o.amount) from sample$Order o where o.customer.id = :customerId");
query.setParameter("customerId", customerId);
result = (BigDecimal) query.getFirstResult();
// commit transaction
tx.commit();
}
return result != null ? result : BigDecimal.ZERO;
}
}
По умолчанию, представления в EntityManager влияют только на загрузку связей, т.е. все локальные атрибуты всегда загружаются.
Вы можете заставить EntityManager загружать частичные сущности, если установите свойство loadPartialEntities представления в true (например, так делает DataManager). Однако, если загружаемая сущность кэшируется, то данный признак игнорируется, и сущность все равно будет загружена со всеми локальными атрибутами.
New
Только что созданный в памяти экземпляр, например: Car car = new Car(). Новый экземпляр может быть передан в EntityManager.persist() для сохранения в БД, при этом он переходит в состояние Managed.
Managed
Загруженный из БД или новый, переданный в EntityManager.persist(), экземпляр. Принадлежит некоторому экземпляру EntityManager, другими словами, находится в его персистентном контексте.
Любые изменения экземпляра в состоянии Managed будут сохранены в БД в случае коммита транзакции, к которой принадлежит данный EntityManager.
Detached
Экземпляр, загруженный из БД и отсоединенный от своего персистентного контекста (вследствие завершения транзакции или сериализации).
Изменения, вносимые в Detached экземпляр, запоминаются в самом этом экземпляре (в полях, добавленных с помощью bytecode enhancement). Эти изменения будут сохранены в БД, только если данный экземпляр будет снова переведен в состояние Managed путем передачи в метод EntityManager.merge().
5.4.4.3. Загрузка по требованию
Загрузка по требованию (lazy loading) позволяет загружать связанные сущности отложенно, т.е. только в момент первого обращения к их свойствам.
Загрузка по требованию в сумме порождает больше запросов к БД, чем жадная загрузка (eager fetching), однако нагрузка при этом растянута во времени.
Загрузка по требованию работает только для экземпляра в состоянии Managed, то есть внутри транзакции, загрузившей данный экземпляр.
5.4.4.4. Выполнение JPQL запросов
Для выполнения JPQL запросов предназначен интерфейс Query, ссылку на который можно получить у текущего экземпляра EntityManager вызовом метода createQuery(). Если запрос предполагается использовать для извлечения сущностей, рекомендуется вызывать createQuery() с передачей типа результата, что приведет к созданию TypedQuery.
Методы Query в основном соответствуют методам стандартного интерфейса JPA javax.persistence.Query. Рассмотрим отличия.
Customer customer = ...;
TypedQuery<Order> query = entityManager.createQuery(
"select o from sales$Order o where o.customer.id = ?1", Order.class);
query.setParameter(1, customer);
Обратите внимание на сравнение в запросе по идентификатору, но передачу в качестве параметра самого экземпляра сущности.
Вариант метода с передачей implicitConversions = false не выполняет такого преобразования.
Если для Query установлено представление, то по умолчанию Query имеет FlushModeType.AUTO, что влияет на случай, когда в текущем персистентном контексте содержатся измененные экземпляры сущностей: эти экземпляры будут сохранены в БД перед выполнением запроса. Другими словами, ORM сначала синхронизирует состояние сущностей в персистентном контексте и в БД, а уже потом выполняет запрос. Этим гарантируется, что в результаты запроса попадут все соответствующие экземпляры, даже если они еще не были сохранены в базе данных явно. Обратной стороной этого является неявный flush, т.е. выполнение команд SQL update для всех измененных в данном контексте сущностей, что может повлиять на производительность.
Если же Query выполняется без представления, то по умолчанию Query имеет FlushModeType.COMMIT, что означает, что неявный flush вызван не будет, и запрос не будет учитывать содержимое текущего персистентного контекста.
В большинстве случаев игнорирование текущего персистентного контекста допустимо, и является предпочтительным поведением, так как не вызывает дополнительных команд SQL. Однако, при использовании представлений существует следующая проблема: если в персистентном контексте есть измененный экземпляр сущности, и выполняется запрос с представлением и FlushModeType.COMMIT, загружающий этот же экземпляр, то изменения будут потеряны. Поэтому по умолчанию мы используем FlushModeType.AUTO для запросов с представлением.
Вы также можете явно установить flush mode с помощью метода setFlushMode() интерфейса Query, чтобы переопределить режим по умолчанию.
Главное отличие JPQL платформы CUBA от стандартного JPA заключается в том, что в CUBA необходимо всегда использовать алиас как в SELECT, так и в UPDATE/DELETE запросах.
|
CUBA |
JPA |
|
UPDATE app$DeliveryAddress e SET e.customer = :target WHERE e.customer = :source |
UPDATE app$DeliveryAddress SET customer = :target WHERE customer = :source |
Если в проекте используется мягкое удаление, то при выполнении JPQL-запроса DELETE FROM для сущности, удалённой через мягкое удаление, будет выброшено исключение. Дело в том, что такой запрос, по сути, будет трансформирован в запрос SQL для удаления всех сущностей, не помеченных для удаления. По умолчанию мягкое удаление не используется, но его можно разрешить с помощью свойства приложения cuba.enableDeleteStatementInSoftDeleteMode.
В таблице ниже описаны функции JPQL, поддерживаемые и не поддерживаемые платформой CUBA.
|
Функция |
Поддерживается |
Пример запроса |
|
Агрегатные функции |
ДА |
SELECT AVG(o.quantity) FROM app$Order o |
|
НЕТ: агрегатные функции со скалярными выражениями (особенность EclipseLink) |
SELECT AVG(o.quantity)/2.0 FROM app$Order o SELECT AVG(o.quantity * o.price) FROM app$Order o | |
|
ALL, ANY, SOME |
ДА |
SELECT emp FROM app$Employee emp WHERE emp.salary > ALL (SELECT m.salary FROM app$Manager m WHERE m.department = emp.department) |
|
Арифметические функции (INDEX, SIZE, ABS, SQRT, MOD) |
ДА |
SELECT w.name FROM app$Course c JOIN c.studentWaitlist w WHERE c.name = 'Calculus' AND INDEX(w) = 0 SELECT w.name FROM app$Course c WHERE c.name = 'Calculus' AND SIZE(c.studentWaitlist) = 1 SELECT w.name FROM app$Course c WHERE c.name = 'Calculus' AND ABS(c.time) = 10 SELECT w.name FROM app$Course c WHERE c.name = 'Calculus' AND SQRT(c.time) = 10.5 SELECT w.name FROM app$Course c WHERE c.name = 'Calculus' AND MOD(c.time, c.time1) = 2 |
|
CASE |
ДА |
SELECT e.name, f.name, CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum ' WHEN f.annualMiles > 25000 THEN 'Gold ' ELSE '' END, 'Frequent Flyer') FROM app$Employee e JOIN e.frequentFlierPlan f |
|
НЕТ: CASE в UPDATE-запросе |
UPDATE app$Employee e SET e.salary = CASE e.rating WHEN 1 THEN e.salary * 1.1 WHEN 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END | |
|
Функции даты и времени (CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP) |
ДА |
SELECT e FROM app$Order e WHERE e.date = CURRENT_DATE |
|
Функции EclipseLink (CAST, REGEXP, EXTRACT) |
ДА |
SELECT EXTRACT(YEAR FROM e.createTs) FROM app$MyEntity e WHERE EXTRACT(YEAR FROM e.createTs) > 2012 SELECT e FROM app$MyEntity e WHERE e.name REGEXP '.*' SELECT CAST(e.number text) FROM app$MyEntity e WHERE e.path LIKE CAST(:ds$myEntityDs.id text) |
|
НЕТ: CAST в запросе GROUP BY |
SELECT e FROM app$Order e WHERE e.amount > 100 GROUP BY CAST(e.orderDate date) | |
|
Операторы типов сущности |
ДА: тип сущности передаётся как параметр |
SELECT e FROM app$Employee e WHERE TYPE(e) IN (:empType1, :empType2) |
|
НЕТ: прямая ссылка на сущность |
SELECT e FROM app$Employee e WHERE TYPE(e) IN (app$Exempt, app$Contractor) | |
|
Вызов функций |
ДА: результат с операторами сравнения |
SELECT u FROM sec$User u WHERE function('DAYOFMONTH', u.createTs) = 1 |
|
НЕТ: прямое использование результата функции |
SELECT u FROM sec$User u WHERE function('hasRoles', u.createdBy, u.login) | |
|
IN |
ДА |
SELECT e FROM Employee e, IN(e.projects) p WHERE p.budget > 1000000 |
|
IS EMPTY для коллекций |
ДА |
SELECT e FROM Employee e WHERE e.projects IS EMPTY |
|
KEY/VALUE |
НЕТ |
SELECT v.location.street, KEY(i).title, VALUE(i) FROM app$VideoStore v JOIN v.videoInventory i WHERE v.location.zipcode = '94301' AND VALUE(i) > 0 |
|
Литералы |
ДА |
SELECT e FROM app$Employee e WHERE e.name = 'Bob' SELECT e FROM app$Employee e WHERE e.id = 1234 SELECT e FROM app$Employee e WHERE e.id = 1234L SELECT s FROM app$Stat s WHERE s.ratio > 3.14F SELECT s FROM app$Stat s WHERE s.ratio > 3.14e32D SELECT e FROM app$Employee e WHERE e.active = TRUE |
|
НЕТ: литералы даты и времени |
SELECT e FROM app$Employee e WHERE e.startDate = {d'2012-01-03'} SELECT e FROM app$Employee e WHERE e.startTime = {t'09:00:00'} SELECT e FROM app$Employee e WHERE e.version = {ts'2012-01-03 09:00:00.000000001'} | |
|
MEMBER OF |
ДА: для полей и запросов |
SELECT d FROM app$Department d WHERE (select e from app$Employee e where e.id = :eParam) MEMBER OF e.employees |
|
НЕТ: для литералов |
SELECT e FROM app$Employee e WHERE 'write code' MEMBER OF e.codes | |
|
NEW в SELECT |
ДА |
SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) FROM app$Customer c JOIN c.orders o WHERE o.count > 100 |
|
NULLIF/COALESCE |
ДА |
SELECT NULLIF(emp.salary, 10) FROM app$Employee emp SELECT COALESCE(emp.salary, emp.salaryOld, 10) FROM app$Employee emp |
|
NULLS FIRST, NULLS LAST в order by |
ДА |
SELECT h FROM sec$GroupHierarchy h ORDER BY h.level DESC NULLS FIRST |
|
Строковые функции (CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE) |
ДА |
SELECT x FROM app$Magazine x WHERE CONCAT(x.title, 's') = 'JDJs' SELECT x FROM app$Magazine x WHERE SUBSTRING(x.title, 1, 1) = 'J' SELECT x FROM app$Magazine x WHERE LOWER(x.title) = 'd' SELECT x FROM app$Magazine x WHERE UPPER(x.title) = 'D' SELECT x FROM app$Magazine x WHERE LENGTH(x.title) = 10 SELECT x FROM app$Magazine x WHERE LOCATE('A', x.title, 4) = 6 SELECT x FROM app$Magazine x WHERE TRIM(TRAILING FROM x.title) = 'D' |
|
НЕТ: TRIM не поддерживается с trim char |
SELECT x FROM app$Magazine x WHERE TRIM(TRAILING 'J' FROM x.title) = 'D' | |
|
Вложенные запросы |
ДА |
SELECT goodCustomer FROM app$Customer goodCustomer WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) FROM app$Customer c) |
|
НЕТ: path-выражения вместо имени сущности в FROM подзапроса |
SELECT c FROM app$Customer c WHERE (SELECT AVG(o.price) FROM c.orders o) > 100 | |
|
TREAT |
ДА |
SELECT e FROM app$Employee e JOIN TREAT(e.projects AS app$LargeProject) p WHERE p.budget > 1000000 |
|
НЕТ: TREAT в WHERE-выражениях |
SELECT e FROM Employee e JOIN e.projects p WHERE TREAT(p as LargeProject).budget > 1000000 |
5.4.4.4.2. Поиск подстроки без учета регистра
Для удобного формирования условия поиска без учета регистра символов и по любой части строки можно использовать префикс (?i) в значении параметра запроса. Например, имеется запрос:
select c from sales$Customer c where c.name like :name
Если в значении параметра name передать строку (?i)%doe%, то при наличии в БД записи со значением John Doe она будет найдена, несмотря на различие в регистре символа. Это произойдет потому, что ORM выполнит SQL с условием вида lower(C.NAME) like ?.
Следует иметь в виду, что при таком поиске индекс, созданный в БД по полю NAME, не используется.
Текст JPQL запроса может включать макросы, которые обрабатываются перед выполнением и превращаются в исполняемый JPQL, дополнительно модифицируя набор параметров.
Макросы решают следующие задачи:
Рассмотрим их подробно:
@between
Имеет вид @between(field_name, moment1, moment2, time_unit) или @between(field_name, moment1, moment2, time_unit, user_timezone), где
Макрос преобразуется в следующее выражение JPQL: field_name >= :moment1 and field_name < :moment2
Пример 1. Покупатель создан сегодня:
select c from sales$Customer where @between(c.createTs, now, now+1, day)
Пример 2. Покупатель создан в течение последних 10 минут:
select c from sales$Customer where @between(c.createTs, now-10, now, minute)
Пример 3. Документы, датированные последними 5 днями и с учетом часового пояса текущего пользователя:
select d from sales$Doc where @between(d.createTs, now-5, now, day, user_timezone)
@today
Имеет вид @today(field_name) или @today(field_name, user_timezone) и обеспечивает формирование условия попадания значения атрибута в текущий день. По сути это частный случай макроса @between.
Пример. Пользователь создан сегодня:
select d from sales$Doc where @today(d.createTs)
@dateEquals
Имеет вид @dateEquals(field_name, parameter) или @dateEquals(field_name, parameter, user_timezone) и позволяет сформировать условие попадания значения поля field_name типа Timestamp в дату, задаваемую параметром parameter.
Пример:
select d from sales$Doc where @dateEquals(d.createTs, :param)
@dateBefore
Имеет вид @dateBefore(field_name, parameter) или @dateBefore(field_name, parameter, user_timezone) и позволяет сформировать условие, что дата значения поля field_name типа Timestamp меньше даты, задаваемой параметром parameter.
Пример:
select d from sales$Doc where @dateBefore(d.createTs, :param)
@dateAfter
Имеет вид @dateAfter(field_name, parameter) или @dateAfter(field_name, parameter, user_timezone) и позволяет сформировать условие, что дата значения поля field_name типа Timestamp больше или равна дате, задаваемой параметром parameter.
Пример:
select d from sales$Doc where @dateAfter(d.createTs, :param)
@enum
Позволяет использовать полное имя константы enum вместо ее идентификатора в БД. Это упрощает поиск использований enum в коде приложения.
Пример:
select r from sec$Role where r.type = @enum(com.haulmont.cuba.security.entity.RoleType.SUPER) order by r.name
5.4.4.5. Выполнение SQL запросов
ORM позволяет выполнять SQL запросы к базе данных, возвращая как списки отдельных полей, так и экземпляры сущностей. Для этого необходимо создать объект Query или TypedQuery вызовом одного из методов EntityManager.createNativeQuery().
Если выполняется выборка отдельных колонок таблицы, то результирующий список будет содержать строки в виде Object[]. Например:
Query query = persistence.getEntityManager().createNativeQuery(
"select ID, NAME from SALES_CUSTOMER where NAME like ?1");
query.setParameter(1, "%Company%");
List list = query.getResultList();
for (Iterator it = list.iterator(); it.hasNext(); ) {
Object[] row = (Object[]) it.next();
UUID id = (UUID) row[0];
String name = (String) row[1];
}
Если выполняется выборка единственной колонки или агрегатной функции, то результирующий список будет содержать эти значения непосредственно:
Query query = persistence.getEntityManager().createNativeQuery(
"select count(*) from SEC_USER where login = #login");
query.setParameter("login", "admin");
long count = (long) query.getSingleResult();
Если вместе с текстом запроса передан класс результирующей сущности, то возвращается TypedQuery и после выполнения производится попытка отображения результатов запроса на атрибуты сущности. Например:
TypedQuery<Customer> query = em.createNativeQuery(
"select * from SALES_CUSTOMER where NAME like ?1",
Customer.class);
query.setParameter(1, "%Company%");
List<Customer> list = query.getResultList();
Следует иметь в виду, что при использовании SQL колонки, соответствующие атрибутам сущностей типа UUID, возвращаются в виде UUID или в виде String, в зависимости от используемой СУБД:
Параметры этого типа также должны задаваться либо как UUID, либо своим строковым представлением, в зависимости от используемой СУБД. Для обеспечения независимости кода от используемой СУБД рекомендуется использовать DbTypeConverter, который обеспечивает конвертацию данных между объектами Java и параметрами и результатами JDBC.
В SQL запросах можно использовать позиционные или именованные параметры. Позиционные параметры обозначаются ? с последующим номером параметра начиная с 1. Именованные параметры обозначаются знаком #. См. примеры выше.
Поведение SQL запросов, возвращающих сущности, и модифицирующих запросов (update, delete), по отношению к текущему персистентному контексту аналогично описанному для JPQL запросов.
См. также Выполнение SQL с помощью QueryRunner.
Entity Listeners предназначены для реакции на события жизненного цикла экземпляров сущностей на уровне Middleware. См. пример использования в сборнике рецептов.
Слушатель представляет собой класс, реализующий один или несколько интерфейсов пакета com.haulmont.cuba.core.listener. Слушатель будет реагировать на события типов, соответствующих реализуемым интерфейсам.
BeforeDetachEntityListener
Метод onBeforeDetach() вызывается перед отделением объекта от EntityManager при коммите транзакции.
Данный слушатель можно использовать, например, для заполнения неперсистентных атрибутов сущности перед отправкой ее на клиентский уровень.
BeforeAttachEntityListener
Метод onBeforeAttach() вызывается перед введением объекта в персистентный контекст при выполнении операции EntityManager.merge().
Данный слушатель можно использовать, например, для заполнения персистентных атрибутов сущности перед сохранением ее в базе данных.
BeforeInsertEntityListener
Метод onBeforeInsert() вызывается перед выполнением вставки записи в БД. В данном методе возможны любые операции с текущим EntityManager.
AfterInsertEntityListener
Метод onAfterInsert() вызывается после выполнения вставки записи в БД, но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.
BeforeUpdateEntityListener
Метод onBeforeUpdate() вызывается перед изменением записи в БД. В данном методе возможны любые операции с текущим EntityManager.
AfterUpdateEntityListener
Метод onAfterUpdate() вызывается после изменения записи в БД, но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.
BeforeDeleteEntityListener
Метод onBeforeDelete() вызывается перед удалением записи из БД (или в случае мягкого удаления - перед изменением записи). В данном методе возможны любые операции с текущим EntityManager.
AfterDeleteEntityListener
Метод onAfterDelete() вызывается после удаления записи из БД (или в случае мягкого удаления - после изменения записи), но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.
Entity Listener должен являться управляемым бином, поэтому в нем можно использовать инжекцию:
@Component("sample_MyEntityListener")
public class MyEntityListener implements
BeforeInsertEntityListener<MyEntity>,
BeforeUpdateEntityListener<MyEntity> {
@Inject
protected Metadata metadata;
@Override
public void onBeforeInsert(MyEntity entity, EntityManager entityManager) {
Foo foo = metadata.create(Foo.class);
...
entity.setFoo(foo);
entityManager.persist(foo);
}
@Override
public void onBeforeUpdate(MyEntity entity, EntityManager entityManager) {
Foo foo = entityManager.find(Foo.class, entity.getFoo().getId());
...
}
}
Все слушатели кроме BeforeAttachEntityListener выполняются внутри транзакции. Это значит, что если внутри слушателя выбрасывается исключение, текущая транзакция откатывается и все изменения в базе данных теряются.
Если вам необходимо выполнить некоторые действия после успешного завершения транзакции, используйте callback-интерфейс TransactionSynchronization фреймворка Spring для того чтобы отложить выполнение до нужной фазы транзакции. Например:
package com.company.sales.service;
import com.company.sales.entity.Customer;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.BeforeInsertEntityListener;
import com.haulmont.cuba.core.listener.BeforeUpdateEntityListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Component("sales_CustomerEntityListener")
public class CustomerEntityListener implements BeforeInsertEntityListener<Customer>, BeforeUpdateEntityListener<Customer> {
@Override
public void onBeforeInsert(Customer entity, EntityManager entityManager) {
printCustomer(entity);
}
@Override
public void onBeforeUpdate(Customer entity, EntityManager entityManager) {
printCustomer(entity);
}
private void printCustomer(Customer customer) {
System.out.println("In transaction: " + customer);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
System.out.println("After transaction commit: " + customer);
}
});
}
}
Регистрация entity listeners
Entity Listener может быть задан двумя способами:
@Entity(...)
@Table(...)
@Listeners("sample_MyEntityListener")
public class MyEntity extends StandardEntity {
...
}
Для всех экземпляров некоторого класса сущности создается один экземпляр слушателя определенного типа, поэтому слушатель не должен иметь состояния.
Если для сущности объявлены несколько слушателей одного типа (например, аннотациями класса сущности и его предков, плюс динамически), то их вызов будет выполняться в следующем порядке:
5.4.5. Управление транзакциями
В данном разделе рассмотрены различные аспекты управления транзакциями в CUBA-приложениях.
5.4.5.1. Программное управление транзакциями
Программное управление транзакциями осуществляется с помощью интерфейса com.haulmont.cuba.core.Transaction, ссылку на который можно получить методами createTransaction() или getTransaction() интерфейса инфраструктуры Persistence.
Метод createTransaction() создает новую транзакцию и возвращает интерфейс Transaction. Последующие вызовы методов commit(), commitRetaining(), end() этого интерфейса управляют созданной транзакцией. Если в момент создания существовала другая транзакция, то она будет приостановлена, и возобновлена после завершения созданной.
Метод getTransaction() вызывает либо создание новой, либо присоединение к текущей транзакции. Если в момент вызова существовала активная транзакция, то метод успешно завершается, и последующие вызовы commit(), commitRetaining(), end() не оказывают никакого влияния на существующую транзакцию. Однако если end() вызван без предварительного вызова commit(), то текущая транзакция помечается как RollbackOnly.
Примеры программного управления транзакцией:
@Inject
private Metadata metadata;
@Inject
private Persistence persistence;
...
// try-with-resources style
try (Transaction tx = persistence.createTransaction()) {
Customer customer = metadata.create(Customer.class);
customer.setName("John Smith");
persistence.getEntityManager().persist(customer);
tx.commit();
}
// plain style
Transaction tx = persistence.createTransaction();
try {
Customer customer = metadata.create(Customer.class);
customer.setName("John Smith");
persistence.getEntityManager().persist(customer);
tx.commit();
} finally {
tx.end();
}
Интерфейс Transaction имеет также метод execute(), принимающий на вход класс-действие или lambda-выражение, которое нужно выполнить в данной транзакции. Это позволяет организовать управление транзакциями в функциональном стиле, например:
UUID customerId = persistence.createTransaction().execute((EntityManager em) -> {
Customer customer = metadata.create(Customer.class);
customer.setName("ABC");
em.persist(customer);
return customer.getId();
});
Customer customer = persistence.createTransaction().execute(em ->
em.find(Customer.class, customerId, "_local"));
Следует иметь в виду, что метод execute() у некоторого экземпляра Transaction можно вызвать только один раз, так как после выполнения кода действия транзакция завершается.
5.4.5.2. Декларативное управление транзакциями
Любой метод управляемого бина Middleware можно пометить аннотацией @org.springframework.transaction.annotation.Transactional, что вызовет автоматическое создание транзакции при вызове этого метода. В таком методе не нужно вызывать Persistence.createTransaction(), а можно сразу получать EntityManager и работать с ним.
Для аннотации @Transactional можно указать параметры, в том числе:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomething() {
}
@Transactional("db1")
public void doSomething() {
}
Декларативное управление транзакциями позволяет уменьшить количество boilerplate кода, однако имеет следующий недостаток: коммит транзакции происходит вне прикладного кода, что часто затрудняет отладку, т.к. скрывается момент отправки изменений в БД и перехода сущностей в состояние Detached. Кроме того, следует иметь в виду, что декларативная разметка сработает только в случае вызова метода контейнером, т.е. вызов транзакционного метода из другого метода того же самого объекта не приведет к старту транзакции.
В связи с этим рекомендуется применять декларативное управление транзакциями только для простых случаев, таких как методы сервисов, читающих некоторый объект и возвращающих его клиенту.
5.4.5.3. Примеры взаимодействия транзакций
5.4.5.3.1. Откат вложенной транзакции
Если вложенная транзакция создана через getTransaction(), то ее откат приведет к невозможности коммита охватывающей транзакции. Например:
void methodA() {
Transaction tx = persistence.createTransaction();
try {
// (1) calling a method creating a nested transaction
methodB();
// (4) at this point an exception will be thrown, because transaction
// is marked as rollback only
tx.commit();
} finally {
tx.end();
}
}
void methodB() {
Transaction tx = persistence.getTransaction();
try {
// (2) let us assume the exception occurs here
tx.commit();
} catch (Exception e) {
// (3) handle it and exit
return;
} finally {
tx.end();
}
}
Если же транзакция в methodB() будет создана через createTransaction(), то ее откат не окажет никакого влияния на коммит охватывающей транзакции в methodA().
5.4.5.3.2. Чтение и изменение данных во вложенной транзакции
Рассмотрим сначала зависимую вложенную транзакцию, создаваемую через getTransaction():
void methodA() {
Transaction tx = persistence.createTransaction();
try {
EntityManager em = persistence.getEntityManager();
// (1) loading an entity with name == "old name"
Employee employee = em.find(Employee.class, id);
assertEquals("old name", employee.getName());
// (2) setting new value to the field
employee.setName("name A");
// (3) calling a method creating a nested transaction
methodB();
// (8) the changes are committed to DB, and
// it will contain "name B"
tx.commit();
} finally {
tx.end();
}
}
void methodB() {
Transaction tx = persistence.getTransaction();
try {
// (4) retrieving the same instance of EntityManager as methodA
EntityManager em = persistence.getEntityManager();
// (5) loading an entity with the same identifier
Employee employee = em.find(Employee.class, id);
// (6) the field value is the new one since we are working with the same
// persistent context, and there are no calls to DB at all
assertEquals("name A", employee.getName());
employee.setName("name B");
// (7) no actual commit is done at this point
tx.commit();
} finally {
tx.end();
}
}
Теперь рассмотрим тот же самый пример с независимой вложенной транзакцией, создаваемой через createTransaction():
void methodA() {
Transaction tx = persistence.createTransaction();
try {
EntityManager em = persistence.getEntityManager();
// (1) loading an entity with name == "old name"
Employee employee = em.find(Employee.class, id);
assertEquals("old name", employee.getName());
// (2) setting new value to the field
employee.setName("name A");
// (3) calling a method creating a nested transaction
methodB();
// (8) an exception occurs due to optimistic locking
// and commit will fail
tx.commit();
} finally {
tx.end();
}
}
void methodB() {
Transaction tx = persistence.createTransaction();
try {
// (4) creating a new instance of EntityManager,
// as this is a new transaction
EntityManager em = persistence.getEntityManager();
// (5) loading an entity with the same identifier
Employee employee = em.find(Employee.class, id);
// (6) the field value is old because an old instance of the entity
// has been loaded from DB
assertEquals("old name", employee.getName());
employee.setName("name B");
// (7) the changes are commited to DB, and the value of
// "name B" will now be in DB
tx.commit();
} finally {
tx.end();
}
}
В последнем случае исключение в точке (8) возникнет, только если сущность является оптимистично блокируемой, т.е. если она реализует интерфейс Versioned.
Для создаваемой транзакции может быть указан таймаут в секундах, при превышении которого транзакция будет прервана и откачена. Таймаут транзакции ограничивает максимальную длительность запросов к базе данных.
При программном управлении транзакциями таймаут включается путем передачи объекта TransactionParams в метод Persistence.createTransaction(). Например:
Transaction tx = persistence.createTransaction(new TransactionParams().setTimeout(2));
При декларативном управлении транзакциями используется параметр timeout аннотации @Transactional, например:
@Transactional(timeout = 2)
public void someServiceMethod() {
...
Таймаут по умолчанию может быть задан в свойстве приложения cuba.defaultQueryTimeoutSec.
Транзакцию можно пометить как read-only если она предназначена только для чтения данных из БД. Например, все методы load в DataManager используют read-only транзакции по умолчанию. Read-only транзакции улучшают производительность системы, потому что платформа не выполняет код, обрабатывающий возможные изменения в сущностях. Кроме того, не вызываются BeforeCommit transaction listeners.
|
Если персистентный контекст read-only транзакции содержит измененные сущности, то при попытке коммита транзакции будет выброшено исключение IllegalStateException. Это означает, что помечать транзакцию как read-only следует только если вы уверены, что она не модифицирует никакие сущности. |
При программном управлении транзакциями признак read-only включается путем передачи объекта TransactionParams в метод Persistence.createTransaction(). Например:
Transaction tx = persistence.createTransaction(new TransactionParams().setReadOnly(true));
При декларативном управлении транзакциями используется параметр readOnly аннотации @Transactional, например:
@Transactional(readOnly = true)
public void someServiceMethod() {
...
5.4.5.5. Transaction Listeners
Transaction listeners предназначены для реакции на события жизненного цикла транзакций. В отличие от entity listeners, они не привязаны к типу сущности и вызываются для каждой транзакции.
Слушатель должен быть управляемым бином, реализующим один или оба интерфейса BeforeCommitTransactionListener и AfterCompleteTransactionListener.
BeforeCommitTransactionListener
Метод beforeCommit() вызывается перед коммитом транзакции после всех entity listeners если транзакция не является read-only. Метод принимает текущий EntityManager и коллекцию сущностей текущего персистентного контекста.
Данный слушатель можно использовать для обеспечения сложных бизнес-правил, вовлекающих различные сущности. В примере ниже атрибут amount сущности Order должен рассчитываться на основе значения discount, находящегося в заказе, и атрибутов price и quantity экземпляров сущности OrderLine, составляющих заказ.
@Component("demo_OrdersTransactionListener")
public class OrdersTransactionListener implements BeforeCommitTransactionListener {
@Inject
private PersistenceTools persistenceTools;
@Override
public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) {
// gather all orders affected by changes in the current transaction
Set<Order> affectedOrders = new HashSet<>();
for (Entity entity : managedEntities) {
// skip not modified entities
if (!persistenceTools.isDirty(entity))
continue;
if (entity instanceof Order)
affectedOrders.add((Order) entity);
else if (entity instanceof OrderLine) {
Order order = ((OrderLine) entity).getOrder();
// a reference can be detached, so merge it into current persistence context
affectedOrders.add(entityManager.merge(order));
}
}
// calculate amount for each affected order by its lines and discount
for (Order order : affectedOrders) {
BigDecimal amount = BigDecimal.ZERO;
for (OrderLine orderLine : order.getOrderLines()) {
if (!orderLine.isDeleted()) {
amount = amount.add(orderLine.getPrice().multiply(orderLine.getQuantity()));
}
}
BigDecimal discount = order.getDiscount().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_DOWN);
order.setAmount(amount.subtract(amount.multiply(discount)));
}
}
}
AfterCompleteTransactionListener
Метод afterComplete() вызывается после завершения транзакции. Метод принимает параметр, указывающий, была ли транзакция успешно закоммичена, и коллекцию detached сущностей, содержавшихся в персистентном контексте завершенной транзакции.
Пример использования:
@Component("demo_OrdersTransactionListener")
public class OrdersTransactionListener implements AfterCompleteTransactionListener {
private Logger log = LoggerFactory.getLogger(OrdersTransactionListener.class);
@Override
public void afterComplete(boolean committed, Collection<Entity> detachedEntities) {
if (!committed)
return;
for (Entity entity : detachedEntities) {
if (entity instanceof Order) {
log.info("Order: " + entity);
}
}
}
}
5.4.6. Кэши сущностей и запросов
Кэш сущностей (Entity Cache)
Кэш сущностей предоставляется ORM фреймворком EclipseLink. Он хранит в памяти недавно прочитанные или записанные экземпляры сущностей, тем самым сокращая доступ к базе данных и увеличивая производительность.
Кэш сущностей используется только при извлечении сущностей по идентификатору, поэтому запросы по другим атрибутам по-прежнему выполняются на базе данных. Тем не менее, эти запросы могут стать проще и быстрее, если связанные сущности находятся в кэше. Например, если вы запрашиваете Заказы вместе со связанными Заказчиками, и не используете кэш, то SQL-запрос будет содержать JOIN с таблицей заказчиков. Если же сущность Заказчик закэширована, SQL-запрос будет только по таблице заказов, а связанные заказчики будут извлечены из кэша.
Для того, чтобы включить кэш сущностей, установите следующие свойства приложения в файле app.properties модуля core вашего проекта:
|
Если кэширование включено, всегда рекомендуется увеличить размер кэша (по умолчанию - 100). В противном случае, если запрос вернёт более 100 записей, то каждая запись будет извлечена отдельной операцией. |
Факт кэширования сущности влияет на то, какой fetch mode выбирается платформой при загрузке графов сущностей. Если некоторый ссылочный атрибут представляет собой кэшируемую сущность, то fetch mode всегда будет UNDEFINED, что позволяет ORM извлекать ссылку из кэша вместо добавления в запрос JOIN или выполнения отдельного batch-запроса.
Платформа обеспечивает координацию кэша сущностей в кластере middleware. Когда кэшированный экземпляр сущности обновляется или удаляется на одном узле кластера, тот же экземпляр на других узлах (если он загружен) будет инвалидирован, что приведет к загрузке свежего состояния из БД при следующей операции с данным экземпляром.
Кэш запросов (Query Cache)
Кэш запросов сохраняет идентификаторы экземпляров сущностей, возвращаемых JPQL-запросами, тем самым естественно дополняя кэш сущностей.
Например, если для сущности sales$Customer разрешен entity cache, и запрос select c from sales$Customer c where c.grade = :grade выполняется первый раз, происходит следующее:
Когда этот же запрос с такими же параметрами выполняется второй раз, платформа находит запрос в кэше запросов и загружает экземпляры сущностей по идентификаторам из кэша сущностей, не обращаясь к базе данных.
Запросы по умолчанию не кэшируются. Указать, что запрос должен кэшироваться, можно на различных уровнях приложения:
|
Кэшируемые запросы следует использовать только если для возвращаемой сущности разрешен entity cache. В противном случае при каждом запросе экземпляры сущности будут загружаться из базы данных по идентификаторам по одному. |
Кэш запросов автоматически инвалидируется, когда через ORM выполняются операции создания, изменения или удаления с сущностями соответствующего типа. Инвалидация работает по всему кластеру среднего слоя.
JMX-бин app-core.cuba:type=QueryCacheSupport можно использовать для мониторинга состояния кэша и для удаления запросов из кэша. Например, если вы изменили некоторый экземпляр сущности sales$Customer напрямую в БД, необходимо удалить все закэшированные запросы по этой сущности с помощью операции evict() с аргументом sales$Customer.
На поведение кэша запросов оказывают влияние следующие свойства приложения:
5.4.7. Системная аутентификация
При выполнении пользовательских запросов программному коду Middleware через интерфейс UserSessionSource всегда доступна информация о текущем пользователе. Это возможно потому, что при получении запроса с клиентского уровня в потоке выполнения автоматически устанавливается соответствующий объект SecurityContext.
Однако существуют ситуации, когда текущий поток выполнения не связан ни с каким пользователем системы: например, при вызове метода бина из планировщика, либо через JMX-интерфейс. Если при этом бин выполняет изменение сущностей в базе данных, то ему потребуется информация о том, кто выполняет изменения, то есть аутентификация.
Такого рода аутентификация называется системной, так как не требует участия пользователя - средний слой приложения просто создает (или использует имеющуюся) пользовательскую сессию, и устанавливает в потоке выполнения соответствующий объект SecurityContext.
Обеспечить системную аутентификацию некоторого участка кода можно следующими способами:
@Inject
protected Authentication authentication;
...
authentication.begin();
try {
// authenticated code
} finally {
authentication.end();
}
@Authenticated
public String foo(String value) {
// authenticated code
}
Во втором случае также используется бин Authentication, но неявно, через интерцептор AuthenticationInterceptor, который перехватывает вызовы всех методов бинов с аннотацией @Authenticated.
В приведенных примерах пользовательская сессия будет создаваться от лица пользователя, логин которого указан в свойстве приложения cuba.jmxUserLogin. Если требуется аутентификация от имени другого пользователя, нужно воспользоваться первым вариантом и передать в метод begin() логин нужного пользователя.
|
Если в момент выполнения Authentication.begin() в текущем потоке выполнения присутствует активная пользовательская сессия, то она не заменяется - соответственно, код, требующий аутентификации, будет выполняться с имеющейся сессией, и последующий метод end() не будет очищать поток. Например, вызов метода JMX-бина из встроенной в Web Client консоли JMX, если бин находится в той же JVM, что и блок WebClient, к которому в данный момент подключен пользователь, будет выполнен от имени текущего зарегистрированного в системе пользователя, независимо от наличия системной аутентификации. |
5.5. Универсальный пользовательский интерфейс
Подсистема универсального пользовательского интерфейса (Generic UI, GUI) позволяет разрабатывать экраны пользовательского интерфейса, используя XML и Java. Созданные таким образом экраны одинаково работоспособны в двух стандартных клиентских блоках: Web Client и Desktop Client.
Рисунок 14. Структура универсального пользовательского интерфейса
Здесь в центре изображены основные составляющие экранов универсального пользовательского интерфейса:
Код экранов приложения, расположенный в модуле gui, взаимодействует с интерфейсами визуальных компонентов (VCL Interfaces), реализованными по отдельности в модулях web и desktop базового проекта cuba. Для Web Client реализация основана на фреймворке Vaadin, для Desktop Client – на фреймворке Java Swing.
Библиотека визуальных компонентов (Visual Components Library, VCL) содержит большой набор готовых компонентов для отображения данных.
Механизм источников данных (Datasources) предоставляет унифицированный интерфейс, обеспечивающий функционирование связанных с данными визуальных компонентов.
Инфраструктура клиента (Infrastructure) включает в себя главное окно приложения, механизмы отображения и взаимодействия экранов UI, а также средства взаимодействия со средним слоем.
Экран универсального пользовательского интерфейса состоит из XML-дескриптора и класса контроллера. Дескриптор содержит ссылку на класс контроллера.
Для того чтобы экран можно было вызывать из главного меню или из Java кода (например, из контроллера другого экрана), XML-дескриптор должен быть зарегистрирован в файле screens.xml проекта. Экран, который должен быть открыт по умолчанию после входа в систему, можно задать с помощью свойства приложения cuba.web.defaultScreenId.
Главное меню приложения формируется отдельно для Web Client и Desktop Client на основе файлов menu.xml, расположенных соответственно в модулях web и desktop проекта.
В данном разделе рассматриваются основные типы экранов:
Фреймы представляют собой части экранов, которые применяются для декомпозиции и многократного использования. Для подключения фрейма в XML экрана используется элемент frame.
Контроллер фрейма должен быть унаследован от класса AbstractFrame.
|
Фрейм можно создать в Studio с помощью шаблона Blank frame. |
Правила взаимодействия экрана и вложенного в него фрейма:
При коммите экрана вызывается также коммит измененных источников данных всех вложенных фреймов.
Простой экран предназначен для отображения и редактирования произвольной информации, в том числе отдельных экземпляров и списков сущностей. Данный тип экрана имеет только базовую функциональность, позволяющую отобразить его в главном окне системы и работать с источниками данных.
Контроллер простого экрана должен быть унаследован от класса AbstractWindow.
|
Простой экран можно создать в Studio с помощью шаблона Blank screen. |
Экран выбора (lookup) предназначен для выбора и возврата экземпляров и списков сущностей. Стандартное действие LookupAction в таких визуальных компонентах, как PickerField и LookupPickerField, вызывает экраны выбора для поиска связанных сущностей.
При вызове экрана выбора методом openLookup() отображается панель с кнопками для выбора. Когда пользователь выбирает один или несколько экземпляров, экран вызывает переданный ему обработчик, тем самым возвращая вызывающему коду результаты выбора. При вызове методом openWindow() или, например, из главного меню, панель с кнопками выбора не отображается, что превращает экран выбора в простой экран.
Контроллер экрана выбора должен быть унаследован от класса AbstractLookup. В XML экрана в атрибуте lookupComponent должен быть указан компонент (например, Table), из которого будет взят экземпляр сущности при выборе.
|
Экран выбора для сущности можно создать в Studio с помощью шаблонов Entity browser или Entity combined screen. |
По умолчанию, действие LookupAction использует экран, зарегистрированный в файле screens.xml с идентификатором вида {имя_сущности}.lookup или {имя_сущности}.browse, например, sales$Customer.lookup. Поэтому при использовании вышеупомянутых компонентов убедитесь, что такой экран создан. Studio регистрирует browse-экраны с идентификаторами вида {имя_сущности}.browse, поэтому они автоматически вызываются в качестве экранов выбора.
Настройка вида и поведения экрана выбора
<layout expand="table">
<hbox>
<button id="selectBtn" caption="Select item"
action="lookupSelectAction"/>
</hbox>
<!-- ... -->
</layout>
@Override
public void init(Map<String, Object> params) {
addAction(new SelectAction(this) {
@Override
protected Collection getSelectedItems(LookupComponent lookupComponent) {
Set<MyEntity> selected = new HashSet<>();
// ...
return selected;
}
});
}
В качестве базового класса используйте com.haulmont.cuba.gui.components.SelectAction, переопределив требуемые методы.
5.5.1.1.4. Экран редактирования
Экран редактирования предназначен для отображения и редактирования экземпляра сущности. Поддерживает функциональность установки редактируемого экземпляра и действия по коммиту изменений в базу данных. Экран редактирования должен вызываться методом openEditor() с передачей экземпляра сущности.
Стандартные действия CreateAction и EditAction открывают экран, зарегистрированный в файле screens.xml с идентификатором вида {имя_сущности}.edit, например, sales$Customer.edit.
Контроллер экрана редактирования должен быть унаследован от класса AbstractEditor.
|
Экран редактирования для сущности можно создать в Studio с помощью шаблона Entity editor. |
В XML экрана в атрибуте datasource указывается источник данных, в который проставляется редактируемый экземпляр сущности. Для отображения действий, выполняющих коммит или отмену изменений, в XML можно использовать следующие стандартные фреймы с кнопками:
В экране редактирования неявно создаются следующие действия:
Таким образом, если в экран добавлен фрейм editWindowActions, то кнопка OK коммитит изменения и закрывает экран, а кнопка Cancel - закрывает без коммита. Если же добавлен фрейм extendedEditWindowActions, то кнопка OK только коммитит изменения, оставляя экран открытым, кнопка OK & Close коммитит и закрывает экран, кнопка Cancel - закрывает без коммита.
Вместо стандартных фреймов для отображения действий можно использовать произвольные компоненты, например, LinkButton.
5.5.1.1.5. Комбинированный экран
Комбинированный экран позволяет отобразить список сущностей слева и форму редактирования выбранного экземпляра справа. Таким образом, это комбинация экранов выбора и редактирования.
Контроллер экрана редактирования должен быть унаследован от класса EntityCombinedScreen.
|
Комбинированный экран для сущности можно создать в Studio с помощью шаблона Entity combined screen. |
XML-дескриптор - это файл формата XML, описывающий источники данных и расположение визуальных компонентов экрана.
Схема XML доступна по адресу http://schemas.haulmont.com/cuba/6.10/window.xsd.
Рассмотрим структуру дескриптора.
window − корневой элемент.
Атрибуты window:
caption="msg://caption"
Элементы window:
Атрибуты dialogMode:
Пример использования dialogMode:
<dialogMode height="600"
width="800"
positionX="200"
positionY="200"
forceDialog="true"
closeOnClickOutside="false"
resizable="true"/>
Элементы companions:
Каждый из этих элементов содержит атрибут class, задающий класс компаньона.
Атрибуты layout:
Контроллер экрана - это Java или Groovy класс, связанный с XML-дескриптором, и содержащий логику инициализации и обработки событий экрана.
Контроллер должен быть унаследован от одного из следующих базовых классов:
|
Если экрану не нужна никакая дополнительная логика, то в качестве контроллера можно использовать сам базовый класс AbstractWindow, AbstractLookup или AbstractEditor, указав его в XML-дескрипторе (эти классы на самом деле не являются абстрактными в смысле невозможности создания экземпляров). Для фрейма класс контроллера можно не указывать вообще. |
Класс контроллера должен быть зарегистрирован в XML-дескрипторе экрана в атрибуте class корневого элемента window.
Рисунок 15. Базовые классы контроллеров
AbstractFrame является корнем иерархии классов контроллеров. Рассмотрим его основные методы:
В метод init() из вызывающего кода передается мэп параметров, которые могут быть использованы внутри контроллера. Эти параметры могут быть переданы как из кода контроллера вызывающего экрана (в методе openWindow(), openLookup() или openEditor()), так и установлены в файле регистрации экранов screens.xml.
Метод init() следует имплементировать при необходимости инициализации компонентов экрана, например:
@Inject
private Table someTable;
@Override
public void init(Map<String, Object> params) {
someTable.addGeneratedColumn("someColumn", new Table.ColumnGenerator<Colour>() {
@Override
public Component generateCell(Colour entity) {
...
}
});
}
@Inject
private BoxLayout container;
@Override
public void init(Map<String, Object> params) {
SomeFrame frame = openFrame(container, "someFrame");
frame.setHeight("100%");
frame.someInitMethod();
}
Контейнер не обязательно сразу передавать в метод openFrame(), вместо этого можно загрузить фрейм, а затем добавить его в нужный контейнер:
@Inject
private BoxLayout container;
@Override
public void init(Map<String, Object> params) {
SomeFrame frame = openFrame(null, "someFrame");
frame.setHeight("100%");
frame.someInitMethod();
container.add(frame);
}
При открытии экрана в режиме диалога метод openWindow() может быть вызван с параметрами, к примеру:
@Override
public void actionPerform(Component component) {
openWindow("sec$User.browse", WindowManager.OpenType.DIALOG.width(800).height(300).closeable(true).resizable(true).modal(false));
}
Эти параметры будут учитываться, если они не конфликтуют с более приоритетными параметрами самого вызываемого экрана. Последние могут быть заданы в методе getDialogOptions() контроллера экрана или в XML-дескрипторе этого экрана:
<dialogMode forceDialog="true" width="300" height="200" closeable="true" modal="true" closeOnClickOutside="true"/>
Для выполнения действий после закрытия вызываемого экрана необходимо добавить слушатель типа CloseListener, например:
CustomerEdit editor = openEditor("sales$Customer.edit", customer, WindowManager.OpenType.THIS_TAB);
editor.addCloseListener((String actionId) -> {
// do something
});
CloseWithCommitListener можно использовать в случае, если необходимо реагировать только при закрытии экрана действием с именем Window.COMMIT_ACTION_ID (то есть кнопкой OK), например:
CustomerEdit editor = openEditor("sales$Customer.edit", customer, WindowManager.OpenType.THIS_TAB);
editor.addCloseWithCommitListener(() -> {
// do something
});
Для отображения стандартных кнопок типа OK, Cancel и других рекомендуется использовать объекты типа DialogAction, например:
showOptionDialog("PLease confirm", "Are you sure?",
MessageType.CONFIRMATION,
new Action[] {
new DialogAction(DialogAction.Type.YES) {
@Override
public void actionPerform(Component component) {
// do something
}
},
new DialogAction(DialogAction.Type.NO)
});
API
formatMessage() - getMessage() - init() - openFrame() - openEditor() - openLookup() - openWindow() - showMessageDialog() - showNotification() - showOptionDialog() - showWebPage()
AbstractWindow является наследником AbstractFrame, и определяет следующие собственные методы:
Установка ширины и высоты:
@Override
public void init(Map<String, Object> params) {
getDialogOptions().setWidth("480px").setHeight("320px");
}
Установка положения диалога на экране:
getDialogOptions()
.setPositionX(100)
.setPositionY(100);
Возможность закрыть диалог кликом по окружающей области:
getDialogOptions().setModal(true).setCloseOnClickOutside(true);
Указание того, что диалог должен быть немодальным и с изменяемыми размерами:
@Override
public void init(Map<String, Object> params) {
getDialogOptions().setModal(false).setResizable(true);
}
Указание того, что диалог должен быть развёрнут во весь экран:
getDialogOptions().setMaximized(true);
Указание того, что экран должен всегда открываться в режиме диалога, независимо от того, какой WindowManager.OpenType был выбран в вызывающем коде:
@Override
public void init(Map<String, Object> params) {
getDialogOptions().setForceDialog(true);
}
Доступны следующие режимы:
К примеру, на экране имеется чекбокс showPanel, управляющий отображением некой панели. Мы переопределяем метод saveSettings(): создаём в нём XML-элемент для этого чекбокса, добавляем ему атрибут showPanel, содержащий текущее значение чекбокса, а затем сохраняем элемент settings в XML-файл для текущего пользователя в базе данных.
@Inject
private CheckBox showPanel;
@Override
public void saveSettings() {
boolean showPanelValue = showPanel.getValue();
Element xmlDescriptor = getSettings().get(showPanel.getId());
xmlDescriptor.addAttribute("showPanel", String.valueOf(showPanelValue));
super.saveSettings();
}
Переопределим метод для восстановления настроек из предыдущего примера. Получаем XML-элемент чекбокса, проверяем, что нужный нам атрибут showPanel не равен null, а затем восстанавливаем для чекбокса предыдущее сохранённое значение:
@Override
public void applySettings(Settings settings) {
super.applySettings(settings);
Element xmlDescriptor = settings.get(showPanel.getId());
if (xmlDescriptor.attribute("showPanel") != null) {
showPanel.setValue(Boolean.parseBoolean(xmlDescriptor.attributeValue("showPanel")));
}
}
Другой пример управления настройками экрана можно увидеть на стандартном экране Server Log в меню Administration приложения CUBA, который автоматически сохраняет и восстанавливает последние открытые пользователем лог-файлы.
Данный метод следует переопределять только в том случае, если необходимо полностью заменить стандартную процедуру валидации экрана. Если же нужно только дополнить ее, достаточно определить специальный шаблонный метод postValidate().
private Pattern pattern = Pattern.compile("\\d");
@Override
protected void postValidate(ValidationErrors errors) {
if (getItem().getAddress().getCity() != null) {
if (pattern.matcher(getItem().getAddress().getCity()).find()) {
errors.add("City name can't contain digits");
}
}
}
@Override
public void showValidationErrors(ValidationErrors errors) {
super.showValidationErrors(errors);
}
Метод принимает строковое значение, передаваемое далее в шаблонный метод preClose() и слушателям CloseListener. Таким образом, заинтересованный код может получить информацию о причине закрытия экрана от кода, инициирующего закрытие. В частности, в экранах редактирования сущностей при закрытии экрана после коммита изменений рекомендуется использовать константу Window.COMMIT_ACTION_ID, без коммита изменений - константу Window.CLOSE_ACTION_ID.
Если какой-либо из источников данных содержит несохраненные изменения, перед закрытием экрана будет выдано диалоговое окно с соответствующим предупреждением. Тип предупреждения можно выбрать с помощью свойства приложения cuba.gui.useSaveConfirmation.
Вариант метода close() с параметром force = true закрывает экран без вызова preClose() и без предупреждения, независимо от наличия несохраненных изменений.
Метод close() возвращает true, если экран был успешно закрыт, и false - если закрытие было прервано.
Если метод preClose() возвращает false, то процесс закрытия экрана прерывается.
addBeforeCloseWithCloseButtonListener(BeforeCloseEvent::preventWindowClose);
addBeforeCloseWithShortcutListener(BeforeCloseEvent::preventWindowClose);
API
addBeforeCloseWithCloseButtonListener() - addBeforeCloseWithShortcutListener() - applySettings() - close() - setContentSwitchMode() - getDialogOptions() - postValidate() - preClose() - ready() - saveSettings() - validateAll()
AbstractLookup базовый класс контроллеров экранов выбора, является наследником AbstractWindow, и определяет следующие собственные методы:
Как правило, компонент выбора устанавливается в XML-дескрипторе экрана, и вызывать данный метод в прикладном коде нет необходимости.
По умолчанию валидатор не установлен.
API
setLookupComponent() - setLookupValidator()
AbstractEditor − базовый класс контроллеров экранов редактирования, является наследником AbstractWindow.
При создании конкретного класса контроллера рекомендуется параметризовать AbstractEditor типом редактируемой сущности. При этом методы getItem() и initNewItem() будут работать с конкретным типом сущности и прикладному коду не потребуется дополнительных приведений типов. Например:
public class CustomerEdit extends AbstractEditor<Customer> {
@Override
protected void initNewItem(Customer item) {
...
AbstractEditor определяет следующие собственные методы:
Если редактируется не новый экземпляр, то в момент открытия экрана он перезагружается из базы данных с необходимым представлением, указанным для главного источника данных.
Изменения, вносимые в экземпляр, возвращаемый getItem(), отражаются на состоянии источника данных, и будут отправлены на Middleware при коммите экрана.
|
Следует иметь в виду, что getItem() возвращает значение только после инициализации экрана методом setItem(). До этого момента, например, в методах init() и initNewItem(), данный метод возвращает null. Однако в методе init() экземпляр сущности, переданный в openEditor(), можно получить из параметров следующим образом: @Override public void init(Map<String, Object> params) { Customer item = WindowParams.ITEM.getEntity(params); // do something } В метод initNewItem() экземпляр передается явно и нужного типа. В обоих случаях полученный экземпляр сущности, если он не новый, будет впоследствии перезагружен, и вносить в него изменения или сохранять в поле для последующего использования не имеет смысла. |
Для инициализации экрана редактирования вместо переопределения setItem() рекомендуется имплементировать специальные шаблонные методы initNewItem() и postInit().
|
Метод initNewItem() вызывается только для нового, только что созданного экземпляра сущности. Если редактируется detached экземпляр, метод не вызывается. |
Данный метод можно имплементировать в контроллере при необходимости инициализации нового экземпляра сущности перед его установкой в источник данных, например:
@Inject
private UserSession userSession;
@Override
protected void initNewItem(Complaint item) {
item.setOpenedBy(userSession.getUser());
item.setStatus(ComplaintStatus.OPENED);
}
Более сложный пример использования initNewItem() приведен в разделе рецептов разработки.
Данный метод можно имплементировать в контроллере для окончательной инициализации экрана, например:
@Inject
private EntityStates entityStates;
@Inject
protected EntityDiffViewer diffFrame;
@Override
protected void postInit() {
if (!entityStates.isNew(getItem())) {
diffFrame.loadVersions(getItem());
}
}
Если используется вариант метода с параметром validate = false, то валидация перед коммитом не производится.
Данный метод не рекомендуется переопределять, лучше использовать специальные шаблонные методы postValidate(), preCommit() и postCommit().
Данный метод не рекомендуется переопределять, лучше использовать специальные шаблонные методы postValidate(), preCommit() и postCommit().
Данный метод можно имплементировать в контроллере. Если метод возвращает false, процесс коммита (и закрытия экрана, если был вызван commitAndClose()), прерывается. Например:
@Override
protected boolean preCommit() {
if (somethingWentWrong) {
showNotification("Something went wrong", NotificationType.WARNING);
return false;
}
return true;
}
Реализация метода по умолчанию, если экран не закрывается, отображает сообщение об успешном коммите изменений и вызывает метод postInit().
Данный метод можно переопределить в контроллере для выполнения некоторых действий после успешного коммита, например:
@Inject
private Datasource<Driver> driverDs;
@Inject
private EntitySnapshotService entitySnapshotService;
@Override
protected boolean postCommit(boolean committed, boolean close) {
if (committed) {
entitySnapshotService.createSnapshot(driverDs.getItem(), driverDs.getView());
}
return super.postCommit(committed, close);
}
Далее приведены диаграммы последовательностей инициализации и различных вариантов коммита экрана редактирования.
Рисунок 16. Инициализация экрана редактирования
Рисунок 17. Коммит и закрытие экрана с фреймом editWindowActions
Рисунок 18. Коммит экрана с фреймом extendedEditWindowActions
Рисунок 19. Коммит и закрытие экрана с фреймом extendedEditWindowActions
API
commit() - commitAndClose() - getItem() - initNewItem() - postCommit() - postInit() - preCommit() - setItem()
5.5.1.3.5. EntityCombinedScreen
EntityCombinedScreen − базовый класс контроллеров комбинированных экранов, является наследником AbstractLookup.
Класс EntityCombinedScreen ищет ключевые компоненты экрана, такие как таблица, field group и некоторые другие, по зашитым в код идентификаторам. Если эти компоненты в вашем экране названы по другому, переопределите protected-методы класса и возвращайте из них ваши идентификаторы, чтобы контроллер мог найти нужные компоненты. См. JavaDocs класса для более подробной информации.
5.5.1.3.6. Инжекция зависимостей контроллеров
В контроллерах можно использовать Dependency Injection для получения ссылок на используемые объекты. Для этого нужно объявить либо поле соответствующего типа, либо метод доступа на запись (setter) с соответствующим типом результата, и добавить ему одну из следующих аннотаций:
Инжектировать в контроллеры можно следующие объекты:
С помощью специальной аннотации @WindowParam можно инжектировать в контроллер параметры, передаваемые в мэп метода init(). Аннотация имеет атрибут name, в котором указывается имя параметра (ключ в мэп), и опциональный атрибут required. Если required = true, то при отсутствии в мэп соответствующего параметра в лог выводится сообщение с уровнем WARNING.
Пример инжекции объекта типа Job, передаваемого в метод init() контроллера:
@WindowParam(name = "job", required = true)
protected Job job;
5.5.1.3.7. Компаньоны контроллеров
Базовые классы контроллеров расположены в модуле gui базового проекта cuba и не содержат ссылок на классы реализации визуальных компонентов (Swing или Vaadin), что дает возможность использовать их в клиентах обоих типов.
В то же время конкретные классы контроллеров могут быть расположены как в модуле gui, так и в web или desktop, в зависимости от применяемых в проекте клиентских блоков и специфики экрана. Если контроллер является универсальным, но для разных типов клиента требуется дополнительная функциональность, ее можно определить в так называемых классах-компаньонах.
Класс-компаньон располагается в модуле клиента соответствующего типа (web или desktop) и реализует интерфейс, задаваемый в использующем его контроллере. Класс компаньона задается в элементе companions XML-дескриптора экрана. Контроллер может получить ссылку на экземпляр компаньона с помощью инжекции или вызовом getCompanion(), и в нужный момент передать ему управление, например, для дополнительной инициализации визуальных компонентов специфичным для данного типа клиента способом.
Например, необходимо раздельно для веб и десктоп клиентов проинициализировать таблицу некоторого экрана. Тогда в контроллере экрана, расположенном в модуле gui, определяем интерфейс компаньона и делегируем ему инициализацию таблицы:
public class CustomerBrowse extends AbstractLookup {
public interface Companion {
void initTable(Table<Customer> table);
}
@Inject
protected Table<Customer> table;
@Inject
protected Companion companion;
@Override
public void init(Map<String, Object> params) {
if (companion != null) {
companion.initTable(table);
}
}
}
В модулях web и desktop создаем соответствующие классы реализации компаньона:
public class WebCustomerBrowseCompanion implements CustomerBrowse.Companion {
@Override
public void initTable(Table<Customer> table) {
com.vaadin.ui.Table webTable = (com.vaadin.ui.Table) WebComponentsHelper.unwrap(table);
// do something specific to Vaadin table
}
}
public class DesktopCustomerBrowseCompanion implements CustomerBrowse.Companion {
@Override
public void initTable(Table<Customer> table) {
javax.swing.JTable desktopTable = (javax.swing.JTable) DesktopComponentsHelper.unwrap(table);
// do something specific to Swing table
}
}
И регистрируем классы реализации компаньона в XML-дескрипторе экрана:
<window ...
class="com.company.sample.gui.customers.CustomerBrowse">
<companions>
<web class="com.company.sample.web.customers.WebCustomerBrowseCompanion"/>
<desktop class="com.company.sample.desktop.customers.DesktopCustomerBrowseCompanion"/>
</companions>
<dsContext>...</dsContext>
<layout>...</layout>
</window>
Так как классы-компаньоны расположены в web и desktop модулях, в них можно использовать метод unwrap() классов WebComponentsHelper и DesktopComponentsHelper для извлечения из интерфейса Table ссылок на реализующие таблицу Vaadin и Swing компоненты, и работать с ними непосредственно.
Указание агента позволяет выбрать экран в зависимости от текущего устройства и параметров его дисплея. Например, можно создать два экрана с различной компоновкой (и, возможно, различной функциональностью), и зарегистрировать их в файле screens.xml с одним идентификатором. Тогда во время выполнения платформа выберет экран, который лучше подходит для дисплея, с которого пользователь работает с приложением.
В платформе предопределены три агента: DESKTOP, TABLET, PHONE. Они заданы следующими классами: DesktopScreenAgent, TabletScreenAgent, PhoneScreenAgent. В проекте приложения можно определить собственные агенты путем создания бинов, реализующих интерфейс ScreenAgent.
Агент указывается для экрана в файле screens.xml. Значением атрибута agent должна быть либо одна из вышеперечисленных констант, либо имя бина, реализующего ScreenAgent.
В Studio агент задается на вкладке Properties дизайнера экранов.
5.5.2. Библиотека визуальных компонентов
|
Меню |
|
|
| |
|
| |
|
Кнопки |
|
|
| |
|
| |
|
| |
|
Текст |
|
|
| |
|
Ввод текста |
|
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
Ввод даты |
|
|
| |
|
| |
|
| |
|
Поля выбора |
|
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
Загрузка |
|
|
| |
|
Таблицы и деревья |
|
|
| |
|
| |
|
| |
|
| |
|
| |
|
Другое |
|
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
|
Компонент AppMenu позволяет динамически управлять элементами главного меню в главном окне приложения.
CUBA Studio предоставляет готовый шаблон главного экрана на основе стандартного экрана mainWindow платформы. Шаблон расширяет класс AppMainWindow и обеспечивает прямой доступ к экземпляру компонента AppMenu:
public class ExtAppMainWindow extends AppMainWindow {
@Override
public void init(Map<String, Object> params) {
super.init(params);
AppMenu.MenuItem item = mainMenu.createMenuItem("shop", "Shop");
AppMenu.MenuItem subItem = mainMenu.createMenuItem("customer", "Customers", null, menuItem -> {
showNotification("Customers menu item clicked", NotificationType.HUMANIZED);
});
item.addChildItem(subItem);
mainMenu.addMenuItem(item, 0);
}
}
Методы интерфейса AppMenu:
Методы интерфейса MenuItem:
API
addMenuItem - createMenuItem - createSeparator - setCommand
Компонент BrowserFrame предназначен для включения веб-страницы на страницу приложения. Это аналог HTML-элемента iframe.
XML-имя компонента: browserFrame
Компонент реализован для блока Web Client.
Пример использования компонента browserFrame в XML-дескрипторе экрана:
<browserFrame id="browserFrame"
height="250px"
width="500px"
align="MIDDLE_CENTER">
<url url="https://doc.cuba-platform.com/manual-6.6/"/>
</browserFrame>
Подобно компоненту Image, BrowserFrame также можно использовать для отображения графического содержимого из различных источников. Тип ресурса можно указать декларативно с помощью элементов browserFrame, перечисленных ниже:
<browserFrame>
<classpath path="com/company/sample/web/screens/myPic.jpg"/>
</browserFrame>
<browserFrame>
<file path="D:\sample\modules\web\web\VAADIN\images\myImage.jpg"/>
</browserFrame>
<browserFrame>
<relativePath path="VAADIN/images/myImage.jpg"/>
</browserFrame>
<browserFrame>
<theme path="../halo/com.company.demo/myPic.jpg"/>
</browserFrame>
<browserFrame>
<url url="http://www.foobar2000.org/"/>
</browserFrame>
Атрибуты browserFrame:
Параметры ресурсов browserFrame:
<browserFrame>
<file bufferSize="1024" path="C:/img.png"/>
</browserFrame>
<browserFrame>
<file cacheTime="2400" path="C:/img.png"/>
</browserFrame>
<browserFrame>
<url url="https://avatars3.githubusercontent.com/u/17548514?v=4&s=200"
mimeType="image/png"/>
</browserFrame>
Методы интерфейса BrowserFrame:
browserFrame.addSourceChangeListener(event ->
showNotification("Content updated"));
BrowserFrame frame = componentsFactory.createComponent(BrowserFrame.class);
try {
frame.setSource(UrlResource.class).setUrl(new URL("http://www.foobar2000.org/"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
Вы можете использовать те же типы ресурсов, что и для компонента Image.
UrlResource resource = browserFrame.createResource(UrlResource.class)
.setUrl(new URL(fromString));
browserFrame.setSource(resource);
Отображение HTML в BrowserFrame:
Компонент BrowserFrame можно использовать для встраивания HTML-разметки в приложение. К примеру, вы можете генерировать HTML на лету, используя пользовательский ввод в качестве ресурса:
<textArea id="textArea"
height="250px"
width="400px"/>
<browserFrame id="browserFrame"
height="250px"
width="500px"/>
textArea.addTextChangeListener(event -> {
byte[] bytes = event.getText().getBytes(StandardCharsets.UTF_8);
browserFrame.setSource(StreamResource.class)
.setStreamSupplier(() -> new ByteArrayInputStream(bytes))
.setMimeType("text/html");
});
Отображение PDF в BrowserFrame:
Кроме HTML, BrowserFrame также может отображать содержимое PDF-файлов. Задайте путь к файлу в качестве ресурса для компонента и укажите для него соответствующий MIME-тип:
@Inject
private BrowserFrame browserFrame;
@Inject
private Resources resources;
@Override
public void init(Map<String, Object> params) {
browserFramePdf.setSource(StreamResource.class)
.setStreamSupplier(() -> resources.getResourceAsStream("/com/company/demo/" +
"web/screens/CUBA_Hands_on_Lab_6.8.pdf"))
.setMimeType("application/pdf");
}
Атрибуты browserFrame
align - alternateText - caption - colspan - description - enable - height - icon - id - responsive - rowspan - stylename - visible - width
Атрибуты ресурсов browserFrame
bufferSize - cacheTime - mimeType
Элементы browserFrame
classpath - file - relativePath - theme - url
API
addSourceChangeListener - createResource - setSource
Кнопка (Button) − компонент, обеспечивающий выполнение действия при нажатии.
XML-имя компонента: button
Компонент кнопки реализован для блоков Web Client и Desktop Client.
Кнопка может содержать текст или значок (или и то и другое). На рисунке ниже отображены разные виды кнопок.
Пример кнопки с названием, взятым из пакета локализованных сообщений, и с всплывающей подсказкой:
<button id="textButton" caption="msg://someAction" description="Press me"/>
Название кнопки задается с помощью атрибута caption, всплывающая подсказка − с помощью атрибута description.
Если атрибут disableOnClick имеет значение true, кнопка будет автоматически отключена после клика по ней. Обычно это делается для того, чтобы предотвратить случайные повторные клики по кнопке. Впоследствии, вы можете снова включить кнопку с помощью вызова метода setEnabled(true).
Атрибут icon указывает на местоположение значка в каталоге темы или имя элемента в используемом наборе значков. Подробную информацию о том, где следует располагать файлы значков, можно прочитать в разделе Значки.
Пример создания кнопки со значком:
<button id="iconButton" caption="" icon="SAVE"/>
Основная функция кнопки − выполнить некоторое действие при нажатии на нее. Определить метод контроллера, который будет вызываться при нажатии на кнопку, можно с помощью атрибута invoke. Значением атрибута должно быть имя метода контроллера, удовлетворяющего следующим условиям:
В качестве примера показано описание кнопки, вызывающей метод someMethod:
<button invoke="someMethod" caption="msg://someButton"/>
В контроллере экрана необходимо определить метод someMethod:
public void someMethod() {
//some actions
}
Атрибут invoke игнорируется, если для кнопки задан атрибут action. Атрибут action содержит имя действия, соответствующего данной кнопке.
Пример кнопки с атрибутом action:
<actions>
<action id="someAction" caption="msg://someAction"/>
</actions>
<layout>
<button action="someAction"/>
Кнопке можно назначить любое действие, имеющееся в каком-либо компоненте, реализующем интерфейс Component.ActionsHolder (это актуально для Table, GroupTable, TreeTable, Tree). Причем неважно, каким образом эти действия добавлены - декларативно в XML-дескрипторе или программно в контроллере. В любом случае для использования такого действия достаточно в атрибуте action указать через точку имя компонента и идентификатор нужного действия. Например, в следующем примере кнопке назначается действие create таблицы coloursTable:
<button action="coloursTable.create"/>
Действие для кнопки можно также создавать программно, в контроллере экрана, используя наследование от класса BaseAction.
|
Если для Button установлен экземпляр Action, то кнопка возьмет из него следующие свои свойства: caption, description, icon, enable, visible. Свойства caption, description и icon будут проставлены из действия только в том случае, если они не установлены в самом Button. Остальные перечисленные свойства действия имеют безусловный приоритет над свойствами кнопки. Если свойства действия меняются уже после установки этого Action для Button, то соответственно меняться будут и свойства Button, то есть кнопка слушает изменение свойств действия. В этом случае меняются и свойства caption, description и icon, причем даже если они изначально были назначены на саму кнопку. |
Атрибут primary позволяет задать подсветку отдельных кнопок. Подсветка автоматически применится к кнопке, если у действия, вызываемого этой кнопкой, атрибут primary имеет значение true.
<button primary="true"
invoke="foo"/>
В теме Hover подсветка доступна по умолчанию; для её активации в теме, основанной на Halo, установите значение true для переменной стиля $cuba-highlight-primary-action.
Далее, в веб-клиенте с темой, основанной на Halo, к компоненту Button можно применить предопределенные стили. Стили задаются в XML-дексрипторе или контроллере экрана с помощью атрибута stylename:
<button id="button"
caption="Friendly button"
stylename="friendly"/>
Чтобы применить стиль программно, выберите одну из констант класса HaloTheme с префиксом компонента BUTTON_:
button.setStyleName(HaloTheme.BUTTON_FRIENDLY);
Атрибуты button
action - align - caption - description - disableOnClick - enable - icon - id - invoke - stylename - tabIndex - visible - width
Предопределенные стили button
borderless - borderless-colored - danger - friendly - huge - icon-align-right - icon-align-top - icon-only - large - primary - quiet - small - tiny
BulkEditor - компонент, позволяющий менять значения атрибутов сразу нескольких выбранных экземпляров сущностей. Компонент представляет собой кнопку, добавляющуюся к таблице или дереву и при нажатии открывающую редактор сущностей.
XML-имя компонента: bulkEditor
Компонент реализован для блоков Web Client и Desktop Client.
Для использования BulkEditor у таблицы или дерева должен быть задан атрибут multiselect="true".
Экран редактирования сущностей генерируется автоматически на основе заданного представления (содержащего только поля данной сущности, в том числе ссылки), динамических атрибутов данной сущности (если есть) и разрешений пользователя. Системные атрибуты в редакторе также не отображаются.
Атрибуты сущности в редакторе сортируются по алфавиту. По умолчанию они пусты. При коммите экрана заданные на экране непустые значения атрибутов проставляются всем выбранным экземплярам сущности.
Редактор позволяет удалить значение определенного поля в БД у всех выбранных сущностей, установив его в null. Для этого необходимо нажать на кнопку
рядом с соответствующим полем. После этого поле становится нередактируемым. Разблокировать поле можно, нажав на кнопку эту же кнопку снова.
Пример описания компонента bulkEditor для таблицы:
<table id="invoiceTable"
multiselect="true"
width="100%">
<actions>
<!-- ... -->
</actions>
<buttonsPanel>
<!-- ... -->
<bulkEditor for="invoiceTable"
exclude="customer"/>
</buttonsPanel>
Атрибуты bulkEditor
includeProperties не распространяется на динамические атрибуты сущности.
Чтобы указать атрибуты декларативно, перечислите их через запятую в дескрипторе экрана:
<bulkEditor for="ordersTable" includeProperties="name, description"/>
Список атрибутов также может быть программно задан в контроллере экрана:
bulkEditor.setIncludeProperties(Arrays.asList("name", "description"));
Атрибуты bulkEditor
align - caption - description - enable - exclude - for - icon - id - includeProperties - loadDynamicAttributes - openType - stylename - tabIndex - useConfirmDialog - visible - width
Компонент Calendar предназначен для организации и отображения событий календаря.
XML-имя компонента: calendar.
Компонент реализован для блока Web Client.
Пример описания компонента в XML-дескрипторе экрана:
<calendar id="calendar"
captionProperty="caption"
startDate="2016-10-01"
endDate="2016-10-31"
height="100%"
width="100%"/>
Режим отображения определяется временным диапазоном календаря, который задаётся его начальной и конечной датой. По умолчанию используется режим отображения недели, работающий с диапазонами до семи дней. Для отображения календаря на один день используйте диапазон в пределах одной календарной даты. Режим отображения месяца применяется, если заданный диапазон превышает одну неделю (семь дней).
Кнопки навигации для перелистывания календаря на одну неделю вперёд/назад по умолчанию отключены. Чтобы кнопки были видны в режиме отображения недели, используйте атрибут navigationButtonsVisible:
<calendar width="100%"
height="100%"
navigationButtonsVisible="true"/>
Атрибуты calendar:
Использование событий календаря:
Для отображения событий в ячейках календаря их можно прямо добавлять в объект Calendar при помощи метода addEvent() или использовать интерфейс CalendarEventProvider. Пример добавления события напрямую:
@Inject
private Calendar calendar;
public void generateEvent(String caption, String description, Date start, Date end, boolean isAllDay, String stylename) {
SimpleCalendarEvent calendarEvent = new SimpleCalendarEvent();
calendarEvent.setCaption(caption);
calendarEvent.setDescription(description);
calendarEvent.setStart(start);
calendarEvent.setEnd(end);
calendarEvent.setAllDay(isAllDay);
calendarEvent.setStyleName(stylename);
calendar.getEventProvider().addEvent(calendarEvent);
}
Метод removeEvent() интерфейса CalendarEventProvider позволяет удалить конкретное событие по его индексу:
CalendarEventProvider eventProvider = calendar.getEventProvider();
List<CalendarEvent> events = new ArrayList<>(eventProvider.getEvents());
eventProvider.removeEvent(events.get(events.size()-1));
Метод removeAllEvents, в свою очередь, удаляет все доступные события:
CalendarEventProvider eventProvider = calendar.getEventProvider();
eventProvider.removeAllEvents();
Интерфейс CalendarEventProvider имеет две готовые реализации: ListCalendarEventProvider (создаваемый по умолчанию) и EntityCalendarEventProvider.
ListCalendarEventProvider заполняется данными с помощью метода addEvent(), принимающего объект CalendarEvent в качестве параметра:
@Inject
private Calendar calendar;
public void addEvents() {
ListCalendarEventProvider listCalendarEventProvider = new ListCalendarEventProvider();
calendar.setEventProvider(listCalendarEventProvider);
listCalendarEventProvider.addEvent(generateEvent(
"Training", "Student training", "2016-10-17 09:00", "2016-10-17 14:00", false, "event-blue"));
listCalendarEventProvider.addEvent(generateEvent(
"Development", "Platform development", "2016-10-17 15:00", "2016-10-17 18:00", false, "event-red"));
listCalendarEventProvider.addEvent(generateEvent(
"Party", "Party with friends", "2016-10-22 13:00", "2016-10-22 18:00", false, "event-yellow"));
}
private SimpleCalendarEvent generateEvent(String caption, String description, String start, String end, Boolean allDay, String style) {
SimpleCalendarEvent calendarEvent = new SimpleCalendarEvent();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
calendarEvent.setCaption(caption);
calendarEvent.setDescription(description);
calendarEvent.setStart(df.parse(start));
calendarEvent.setEnd(df.parse(end));
calendarEvent.setAllDay(allDay);
calendarEvent.setStyleName(style);
return calendarEvent;
}
EntityCalendarEventProvider получает данные напрямую из атрибутов сущности. Чтобы EntityCalendarEventProvider мог использовать сущность, она должна иметь как минимум следующие атрибуты: дата начала события (тип DateTime), дата окончания события (тип DateTime) и заголовок события (тип String).
В следующем примере мы предположим, что сущность в источнике данных имеет все необходимые атрибуты: eventCaption, eventDescription, eventStartDate, eventEndDate, eventStylename, и укажем их имена в качестве значений атрибутов calendar:
<calendar id="calendar"
datasource="calendarEventsDs"
width="100%"
height="100%"
startDate="2016-10-01"
endDate="2016-10-31"
captionProperty="eventCaption"
descriptionProperty="eventDescription"
startDateProperty="eventStartDate"
endDateProperty="eventEndDate"
stylenameProperty="eventStylename"/>
Для пользовательского взаимодействия с элементами Calendar, такими как подписи даты и номера недель, выбор диапазона даты/времени, перетаскивание событий и изменение их размера, могут быть заданы различные слушатели. Слушатели также используются для кнопок навигации, листающих диапазон календаря вперёд и назад. Ниже приведён список слушателей по умолчанию:
calendar.addDateClickListener(
calendarDateClickEvent ->
showNotification(String.format("Date clicked: %s", calendarDateClickEvent.getDate().toString()),
NotificationType.HUMANIZED));
Событиям календаря можно задавать стили с помощью CSS. Для настройки стиля задайте имя стиля и его параметры в файле .scss. Пример настройки цвета фона события:
.v-calendar-event.event-green {
background-color: #c8f4c9;
color: #00e026;
}
Затем вызовите метод setStyleName для нужного события:
calendarEvent.setStyleName("event-green");
В результате, цвет фона события стал зелёным:
Для компонента Calendar можно изменить названия дней недели и месяцев по умолчанию, используя методы setDayNames() и setMonthNames():
Map<DayOfWeek, String> days = new HashMap<>(7);
days.put(DayOfWeek.MONDAY,"Heavens and earth");
days.put(DayOfWeek.TUESDAY,"Sky");
days.put(DayOfWeek.WEDNESDAY,"Dry land");
days.put(DayOfWeek.THURSDAY,"Stars");
days.put(DayOfWeek.FRIDAY,"Fish and birds");
days.put(DayOfWeek.SATURDAY,"Animals and man");
days.put(DayOfWeek.SUNDAY,"Rest");
calendar.setDayNames(days);
Атрибуты calendar
caption - captionProperty - colspan - datasource - description - descriptionProperty - endDateProperty - endDate - height - icon - id - isAllDayProperty - rowspan - startDate - startDateProperty - stylename - stylenameProperty - timeFormat - visible - width
API
addEvent - removeAllEvents - removeEvent - setEventProvider - setDayNames
Слушатели calendar
CalendarBackwardClickListener - CalendarDateClickListener - CalendarEventClickListener - CalendarEventMoveListener - CalendarEventResizeListener - CalendarForwardClickListener - CalendarRangeSelectListener - CalendarWeekClickListener
Поле, отображающее состояние клавиши Caps Lock при вводе пароля в поле PasswordField.
XML-имя компонента: capsLockIndicator.
CapsLockIndicator реализован для блоков Web Client и Desktop Client.
Атрибуты capsLockOnMessage и capsLockOffMessage позволяют задать сообщения, которые будут отображаться компонентом в зависимости от того, нажата ли клавиша Caps Lock.
Примеры использования:
<hbox spacing="true">
<passwordField id="passwordField"
capsLockIndicator="capsLockIndicator"/>
<capsLockIndicator id="capsLockIndicator"/>
</hbox>
CapsLockIndicator capsLockIndicator = componentsFactory.createComponent(CapsLockIndicator.class);
capsLockIndicator.setId("capsLockIndicator");
passwordField.setCapsLockIndicator(capsLockIndicator);
Компонент CapsLockIndicator предназначен для использования совместно с PasswordField и отслеживает состояние Caps Lock только тогда, когда поле ввода пароля находится в фокусе. Когда поле теряет фокус, состояние Caps Lock автоматически становится неактивным.
Динамическое изменение видимости компонента CapsLockIndicator с помощью атрибута visible после открытия экрана может работать некорректно.
Атрибуты capsLockIndicator
align - capsLockOffMessage - capsLockOnMessage - colspan - height - id - rowspan - stylename - visible - width
Флажок (CheckBox) − компонент, имеющий два состояния: выбран, не выбран.
XML-имя компонента: checkBox.
Компонент CheckBox реализован для блоков Web Client и Desktop Client.
Пример флажка с надписью, взятой из пакета локализованных сообщений:
<checkBox id="accessField" caption="msg://accessFieldCaption"/>
Сброс или установка флажка изменяет его значение: Boolean.TRUE или Boolean.FALSE. Значение может быть получено с помощью метода getValue() и установлено с помощью метода setValue(). Если в setValue() передать null, то устанавливается значение Boolean.FALSE и флажок снимается.
Изменение значения флажка, так же как и любого другого компонента, реализующего интерфейс Field, можно отслеживать с помощью слушателя ValueChangeListener. Например:
@Inject
private CheckBox accessField;
@Override
public void init(Map<String, Object> params) {
accessField.addValueChangeListener(event -> {
if (Boolean.TRUE.equals(event.getValue())) {
showNotification("set", NotificationType.HUMANIZED);
} else {
showNotification("not set", NotificationType.HUMANIZED);
}
});
}
Для создания флажка, связанного с данными, необходимо использовать атрибуты datasource и property.
<dsContext>
<datasource id="customerDs" class="com.sample.sales.entity.Customer" view="_local"/>
</dsContext>
<layout>
<checkBox datasource="customerDs" property="active"/>
Как видно из примера, в экране описывается источник данных customerDs для некоторой сущности Покупатель (Customer), имеющей атрибут active. В компоненте checkBox в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено флажком. Атрибут должен быть типа Boolean. Значением атрибута может быть null, при этом флажок снимается.
Атрибуты checkBox
align - caption - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - property - stylename - tabIndex - visible - width
API
addValueChangeListener commit - discard - isModified - setContextHelpIconClickHandler
ColorPicker представляет собой поле для предпросмотра и выбора цвета. Компонент возвращает шестнадцатеричный (HEX) код цвета в виде строки.
Пример использования ColorPicker с надписью, взятой из пакета локализованных сообщений:
<colorPicker id="colorPicker" caption="msg://colorPickerCaption"/>
Пример ColorPicker с закрытым окном палитры.
Для создания ColorPicker, связанного с данными, необходимо использовать атрибуты datasource и property.
<dsContext>
<datasource id="carsDs" class="com.sample.sales.entity.Cars" view="_local"/>
</dsContext>
<layout>
<colorPicker id="colorPicker" datasource="carsDs" property="color"/>
Атрибуты colorPicker:
Видимость вкладок окна палитры можно определить с помощью атрибутов:
По умолчанию включена только вкладка селектора RGB.
Надписи окна палитры можно переопределить:
Метод компонента getValue() возвращает строку, содержащую HEX-код цвета.
Атрибуты colorPicker
align - buttonCaption - cancelButtonCaption - caption - confirmButtonCaption - contextHelpText - contextHelpTextHtmlEnabled - datasource - defaultCaptionEnabled - editable - height - historyVisible - hsvVisible - icon - id - lookupAllCaption - lookupBlueCaption - lookupGreenCaption - lookupRedCaption - popupCaption - rgbVisible - required - stylename - swatchesTabCaption - swatchesVisible - tabIndex - visible - width
API
ContextHelpIconClickHandler - ValueChangeListener -
CurrencyField - это разновидность текстового поля, предназначенная для ввода денежных единиц. Поле содержит ярлык с обозначением валюты и по умолчанию имеет выравнивание по правому краю.
XML-имя компонента: currencyField.
Компонент CurrencyField реализован только для блока Web Client.
CurrencyField в основном повторяет функциональность TextField: вы так же можете указать тип данных для поля, за исключением того, что CurrencyField поддерживает только числовые типы данных, унаследованные от NumericDatatype. Если установлен иной тип данных, будет выброшено исключение.
CurrencyField можно привязать к источнику данных с помощью атрибутов datasource и property:
<currencyField currency="$"
datasource="orderDs"
property="amount"/>
Компонент currencyField имеет следующие специфические атрибуты:
<currencyField currency="USD"/>
Атрибуты currencyField
align - caption - colspan - contextHelpText - contextHelpTextHtmlEnabled - currency - currencyLabelPosition - datasource - datatype - description - editable - enable - height - icon - id - property - required - requiredMessage - rowspan - showCurrencyLabel - stylename - visible - width
Предопределенные стили currencyField
API
addValidator - addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler - setCurrency - setCurrencyLabelPosition - setShowCurrencyLabel
DataGrid, подобно компоненту Table, позволяет выводить информацию в виде таблицы, сортировать её, вызывать действия для выбранных строк, а также более эффективно управлять строками и колонками таблицы за счёт отложенной загрузки данных при прокрутке.
XML-имя компонента: dataGrid.
Компонент реализован для блока Web Client.
Пример описания компонента в XML-дескрипторе экрана:
<dsContext>
<collectionDatasource id="ordersDs"
class="com.sample.sales.entity.Order"
view="order-with-customer">
<query>
select o from sales$Order o order by o.date
</query>
</collectionDatasource>
</dsContext>
<dataGrid id="ordersDataGrid"
datasource="ordersDs"
height="100%"
width="100%">
<columns>
<column id="date" property="date"/>
<column id="customerName" property="customer.name"/>
<column id="amount" property="amount"/>
</columns>
</dataGrid>
В данном примере атрибут id - это идентификатор колонки, а атрибут property содержит имя атрибута сущности, содержащейся в источнике данных, который следует использовать в качестве данных для колонки.
Элементы dataGrid:
<columns>
<column id="date" property="date"/>
<column id="customer" property="customer"/>
<column id="customerName" property="customer.name"/>
<column id="customerCountry" property="customer.address.country"/>
</columns>
Элемент column может содержать вложенный элемент formatter для представления значения атрибута в виде, отличном от стандартного для данного DataType:
<column id="date">
<formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter"
format="yyyy-MM-dd HH:mm:ss"
useUserTimezone="true"/>
</column>
Компонент RowsCount может также отобразить общее число записей, возвращаемых текущим запросом в источнике данных, без извлечения этих записей. Для этого при щелчке пользователя на знаке "?" он вызывает метод AbstractCollectionDatasource.getCount(), что приводит к выполнению в БД запроса с такими же, как у текущего запроса, условиями, но с агрегатной функцией COUNT(*) вместо результатов. Полученное число отображается вместо знака "?".
Атрибуты dataGrid:
Изменение размера колонок можно отслеживать с помощью слушателя ColumnResizeListener.
Скрытие и отображение колонок можно отслеживать с помощью слушателя ColumnCollapsingChangeListener.
Щелчки правой кнопкой мыши по области компонента DataGrid можно отслеживать с помощью слушателя ContextClickListener.
Изменение расположения колонок можно отслеживать с помощью слушателя ColumnReorderListener.
Выделение строк можно отслеживать с помощью слушателя SelectionListener.
События сортировки DataGrid можно отслеживать с помощью слушателя SortListener.
Методы интерфейса DataGrid:
Если такое действие найдено и имеет свойство enabled = true, оно выполняется.
Если такое действие найдено и имеет свойство enabled = true, оно выполняется.
События клика по элементу DataGrid можно отслеживать с помощью слушателя ItemClickListener.
Использование всплывающих подсказок:
customersDataGrid.setCellDescriptionProvider((entity,columnId)->{
if ("name".equals(columnId)||"lastName".equals(columnId)){
return null;
}
String description="<strong>"+
messages.getTools().getPropertyCaption(entity.getMetaClass(),columnId)+
": </strong>";
if ("grade".equals(columnId)){
description += messages.getMessage(entity.getGrade());
} else if ("active".equals(columnId)){
description += getMessage(entity.getActive() ? "trueString":"falseString");
} else {
description += entity.getValue(columnId);
}
return description;
});
customersDataGrid.setRowDescriptionProvider(Instance::getInstanceName);
Использование интерфейса DetailsGenerator:
Интерфейс DetailsGenerator позволяет задать свой компонент для отображения информации о выбранной строке DataGrid с помощью метода setDetailsGenerator():
ordersGrid.setDetailsGenerator(new DataGrid.DetailsGenerator<Order>() {
@Nullable
@Override
public Component getDetails(Order entity) {
VBoxLayout mainLayout = componentsFactory.createComponent(VBoxLayout.class);
mainLayout.setWidth("100%");
mainLayout.setMargin(true);
HBoxLayout headerBox = componentsFactory.createComponent(HBoxLayout.class);
headerBox.setWidth("100%");
Label infoLabel = componentsFactory.createComponent(Label.class);
infoLabel.setHtmlEnabled(true);
infoLabel.setStyleName("h1");
infoLabel.setValue("Order info:");
Component closeButton = createCloseButton(entity);
headerBox.add(infoLabel);
headerBox.add(closeButton);
headerBox.expand(infoLabel);
Component contentLabel = getContentLabel(entity);
mainLayout.add(headerBox);
mainLayout.add(contentLabel);
mainLayout.expand(contentLabel);
return mainLayout;
}
});
Результат:
Использование режима внутристрочного редактирования:
У компонента DataGrid есть API, позволяющий напрямую редактировать записи в ячейках. Во время редактирования ячейки будет отображён UI с кнопками для сохранения и отмены изменений.
Методы API встроенного редактора:
Вы также можете добавить к встроенному редактору или удалить слушатели, использовав следующие методы:
Данный слушатель обрабатывает событие открытия встроенного редактора DataGrid по двойному щелчку и позволяет получить доступ к полям редактируемой строки. Это даёт возможность обновлять значения в отдельных полях в зависимости от изменения значений в других полях, не закрывая встроенный редактор.
Например:
customersTable.addEditorOpenListener(event -> {
Map<String, Field> fieldMap = event.getFields();
Field active = fieldMap.get("active");
Field grade = fieldMap.get("grade");
ValueChangeListener listener = e ->
active.setValue(true);
grade.addValueChangeListener(listener);
});
Коммит изменений сохраняет их в источнике данных. Логику сохранения изменений в базу данных необходимо задать отдельно.
Само поле редактирования также может быть изменено с помощью интерфейса ColumnEditorFieldGenerator. Используйте метод setEditorFieldGenerator() для определённой колонки таблицы, чтобы указать компонент для отображения в режиме редактирования этой колонки:
ordersGrid.getColumnNN("amount").setEditorFieldGenerator((datasource, property) -> {
LookupField lookupField = componentsFactory.createComponent(LookupField.class);
lookupField.setDatasource(datasource, property);
lookupField.setOptionsList(Arrays.asList(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN));
return lookupField;
});
Результат:
Использование интерфейса ColumnGenerator:
DataGrid имеет возможность добавлять генерируемые, или высчитываемые, колонки. Для этого существует два метода:
ColumnGenerator - это специальный интерфейс, который описывает генерируемую колонку:
Например, для добавления генерируемой колонки, которая будет отображать логин пользователя в верхнем регистре, можно использовать следующий код:
@Override
public void init(Map<String, Object> params){
DataGrid.Column column = usersGrid.addGeneratedColumn("loginUpperCase",new DataGrid.ColumnGenerator<User, String>(){
@Override
public String getValue(DataGrid.ColumnGeneratorEvent<User> event){
return event.getItem().getLogin().toUpperCase();
}
@Override
public Class<String> getType(){
return String.class;
}
},1);
column.setCaption("Login Upper Case");
}
Результат:
ColumnGeneratorEvent, который передается в getValue, хранит информацио о сущности, которая отображается в текущей строке DataGrid, и идентификатор колонки.
По умолчанию, генерируемая колонка добавляется в конец таблицы. Управлять расположением генерируемых колонок можно либо вставляя колонку по индексу, либо предварительно добавив колонку в XML с id, который потом передавать в метод addGeneratedColumn.
Отображение данных в колонках может быть изменено с помощью рендереров. Предположим, что нам необходимо показывать изображение в строке. Тогда текстовое значение пути до изображения можно отобразить в виде изображения с помощью ImageRenderer:
@Override
public void init(Map<String, Object> params){
DataGrid.Column avatar = usersGrid.addGeneratedColumn("userAvatar", new DataGrid.ColumnGenerator<User, String>() {
@Override
public String getValue(DataGrid.ColumnGeneratorEvent<User> event) {
return "icons/user.png";
}
@Override
public Class<String> getType() {
return String.class;
}
}, 0);
avatar.setCaption("Avatar");
avatar.setRenderer(usersGrid.createRenderer(DataGrid.ImageRenderer.class));
}
Результат:
Интерфейс WebComponentRenderer позволяет настроить отображение веб-компонентов различных типов в ячейках DataGrid. Интерфейс реализован только для блока Web Module. Ниже приведён пример создания колонки для отображения компонента LookupField:
import com.haulmont.cuba.core.global.Configuration;
import com.haulmont.cuba.core.global.GlobalConfig;
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.DataGrid;
import com.haulmont.cuba.gui.components.LookupField;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import com.haulmont.cuba.security.entity.User;
import com.haulmont.cuba.web.gui.components.renderers.WebComponentRenderer;
import javax.inject.Inject;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
public class Users extends AbstractWindow {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private Configuration configuration;
@Inject
private DataGrid<User> usersGrid;
@Override
public void init(Map<String, Object> params) {
Map<String, Locale> locales = configuration.getConfig(GlobalConfig.class).getAvailableLocales();
Map<String, Object> options = new TreeMap<>();
for (Map.Entry<String, Locale> entry : locales.entrySet()) {
options.put(entry.getKey(), messages.getTools().localeToString(entry.getValue()));
}
DataGrid.Column column = usersGrid.addGeneratedColumn("language",
new DataGrid.ColumnGenerator<User, Component>() {
@Override
public Component getValue(DataGrid.ColumnGeneratorEvent<User> event) {
LookupField component = componentsFactory.createComponent(LookupField.class);
component.setOptionsMap(options);
component.setWidth("100%");
User user = event.getItem();
component.setValue(user.getLanguage());
component.addValueChangeListener(e -> user.setLanguage((String) e.getValue()));
return component;
}
@Override
public Class<Component> getType() {
return Component.class;
}
});
column.setRenderer(new WebComponentRenderer());
}
}
Результат:
Когда тип поля не совпадает с типом данных, принимаемых рендерером, удобно пользоваться конвертерами, которые обеспечивают конвертацию между типами данных модели и представления. К примеру, чтобы отобразить булево значение в виде значка, можно использовать HtmlRenderer, который умеет отображать HTML-разметку, и конвертер, который будет превращать булево значение в подходящую разметку для отображения значков.
@Override
public void init(Map<String, Object> params){
DataGrid.Column hasEmail = usersGrid.addGeneratedColumn("hasEmail", new DataGrid.ColumnGenerator<User, Boolean>() {
@Override
public Boolean getValue(DataGrid.ColumnGeneratorEvent<User> event) {
return StringUtils.isNotEmpty(event.getItem().getEmail());
}
@Override
public Class<Boolean> getType() {
return Boolean.class;
}
});
hasEmail.setCaption("Has Email");
hasEmail.setRenderer(usersGrid.createRenderer(DataGrid.HtmlRenderer.class));
hasEmail.setConverter(new DataGrid.Converter<String, Boolean>() {
@Override
public Boolean convertToModel(String value, Class<? extends Boolean> targetType, Locale locale) {
return null;
}
@Override
public String convertToPresentation(Boolean value, Class<? extends String> targetType, Locale locale) {
return BooleanUtils.isTrue(value)
? FontAwesome.CHECK_SQUARE_O.getHtml()
: FontAwesome.SQUARE_O.getHtml();
}
@Override
public Class<Boolean> getModelType() {
return Boolean.class;
}
@Override
public Class<String> getPresentationType() {
return String.class;
}
});
}
Результат:
Создавать рендереры можно двумя способами:
dataGrid.createRenderer(DataGrid.ImageRenderer.class) → new WebImageRenderer()
На данный момент этот способ реализован только для модуля Web.
Список рендереров, реализованных в платформе:
Header и Footer:
Интерфейсы HeaderRow и FooterRow предназначены для отображения ячеек заголовков и строк с итогами таблицы соответственно. Эти ячейки могут быть объединёнными для нескольких колонок.
Для создания и настройки заголовков и итогов используются следующие методы:
Интерфейсы HeaderCell и FooterCell позволяют управлять статическими ячейками:
Ниже приведён пример таблицы DataGrid с заголовком, содержащим объединённые ячейки, и строкой итогов, в которой отображаются вычисляемые значения:
<dataGrid id="dataGrid"
datasource="countryGrowthDs"
width="100%">
<columns>
<column property="country"/>
<column property="year2014"/>
<column property="year2015"/>
</columns>
</dataGrid>
public class DataGridHeaderFooterFrame extends AbstractFrame {
@Inject
private DataGrid<CountryGrowth> dataGrid;
@Inject
private CollectionDatasource<CountryGrowth, UUID> countryGrowthDs;
@Inject
private UserSessionSource userSessionSource;
private DecimalFormat percentFormat;
@Override
public void init(Map<String, Object> params) {
countryGrowthDs.refresh();
initPercentFormat();
initHeader();
initFooter();
initRenderers();
}
private DecimalFormat initPercentFormat() {
percentFormat = (DecimalFormat) NumberFormat.getPercentInstance(userSessionSource.getLocale());
percentFormat.setMultiplier(1);
percentFormat.setMaximumFractionDigits(2);
return percentFormat;
}
private void initRenderers() {
dataGrid.getColumnNN("year2014").setRenderer(new WebNumberRenderer(percentFormat));
dataGrid.getColumnNN("year2015").setRenderer(new WebNumberRenderer(percentFormat));
}
private void initHeader() {
HeaderRow headerRow = dataGrid.prependHeaderRow();
HeaderCell headerCell = headerRow.join("year2014", "year2015");
headerCell.setText("GDP growth");
headerCell.setStyleName("center-bold");
}
private void initFooter() {
FooterRow footerRow = dataGrid.appendFooterRow();
footerRow.getCell("country").setHtml("<strong>" + getMessage("average") + "</strong>");
footerRow.getCell("year2014").setText(percentFormat.format(getAverage("year2014")));
footerRow.getCell("year2015").setText(percentFormat.format(getAverage("year2015")));
}
private double getAverage(String propertyId) {
double average = 0.0;
Collection<CountryGrowth> items = countryGrowthDs.getItems();
for (CountryGrowth countryGrowth : items) {
Double value = countryGrowth.getValue(propertyId);
average += value != null ? value : 0.0;
}
return average / items.size();
}
}
Атрибуты dataGrid
align - caption - colspan - columnResizeMode - columnsCollapsingAllowed - contextMenuEnabled - datasource - description - editorBuffered - editorCancelCaption - editorEnabled - editorSaveCaption - enable - frozenColumnCount - headerVisible - height - icon - id - reorderingAllowed - responsive - rowspan - selectionMode - settingsEnabled - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
Элементы dataGrid
actions - buttonsPanel - columns - rowsCount
Атрибуты column
caption - collapsed - collapsible - collapsingToggleCaption - editable - expandRatio - id - maximumWidth - minimumWidth - property - resizable - sortable - width
Элементы column
API
addGeneratedColumn - applySettings - createRenderer - edit - saveSettings - getColumns - setCellDescriptionProvider - setCellStyleProvider - setConverter - setDetailsGenerator - setEnterPressAction - setItemClickAction - setRenderer - setRowDescriptionProvider - setRowStyleProvider - sort
Слушатели dataGrid
ColumnCollapsingChangeListener - ColumnReorderListener - ColumnResizeListener - ContextClickListener - EditorCloseListener - EditorOpenListener - EditorPostCommitListener - EditorPreCommitListener - ItemClickListener - SelectionListener - SortListener
Поле для отображения и ввода даты и времени. Представляет собой поле даты, внутри которого имеется кнопка с выпадающим календарем, а правее находится поле для ввода времени.
XML-имя компонента: dateField.
Компонент DateField реализован для блоков Web Client и Desktop Client.
<dsContext>
<datasource id="orderDs" class="com.sample.sales.entity.Order" view="_local"/>
</dsContext>
<layout>
<dateField datasource="orderDs" property="date"/>
Как видно из примера, в экране описывается источник данных orderDs для некоторой сущности Заказ (Order), имеющей атрибут date. В компоненте ввода даты в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в поле.
Формат задается по правилам класса SimpleDateFormat (http://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). Если в формате отсутствуют символы H или h, то поле времени не выводится.
<dateField dateFormat="MM/yy" caption="msg://monthOnlyDateField"/>
|
DateField в основном предназначен для быстрого ввода с клавиатуры путем заполнения маски. Поэтому компонент поддерживает только форматы с цифрами и разделителями. Сложные форматы с текстовым представлением дня недели или месяца не будут работать. |
<dateField id="dateField" rangeStart="2016-08-15" rangeEnd="2016-08-19"/>
Если resolution="DAY" и не указан атрибут dateFormat, то в качестве формата будет взят формат, указанный в главном пакете сообщений с ключом dateFormat.
Если resolution="MIN" и не указан атрибут dateFormat, то в качестве формата будет взят формат, указанный в главном пакете сообщений с ключом dateTimeFormat.
Ниже показано определения поля для ввода даты с точностью до месяца.
<dateField resolution="MONTH" caption="msg://monthOnlyDateField"/>
<dateField id="dateField"
stylename="borderless"/>
Чтобы применить стиль программно, выберите константу класса HaloTheme с префиксом компонента DATEFIELD_:
dateField.setStyleName(HaloTheme.DATEFIELD_BORDERLESS);
Атрибуты dateField
align - caption - contextHelpText - contextHelpTextHtmlEnabled - datasource - dateFormat - description - editable - enable - height - icon - id - property - stylename - rangeEnd - rangeStart - required - requiredMessage - resolution - tabIndex - visible - width
Элементы dateField
Предопределенные стили dateField
borderless - small - tiny
API
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
DatePicker это компонент для выбора даты. Он выглядит так же, как выпадающий календарь в DateField.
XML-имя компонента: datePicker.
DatePicker реализован для блока Web Client.
<dsContext>
<datasource id="orderDs" class="com.sample.sales.entity.Order" view="_local"/>
</dsContext>
<layout>
<datePicker id="datePicker" datasource="orderDs" property="date"/>
Как видно из примера, в экране описывается источник данных orderDs для некоторой сущности Заказ (Order), имеющей атрибут date. В компоненте ввода даты в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в компоненте.
<datePicker id="datePicker" rangeStart="2016-08-15" rangeEnd="2016-08-19"/>
<datePicker id="datePicker" resolution="MONTH"/>
<datePicker id="datePicker" resolution="YEAR"/>
Атрибуты datePicker
align - caption - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - id - property - rangeEnd - rangeStart - resolution - stylename - tabIndex - visible - width
API
addValueChangeListener - setContextHelpIconClickHandler
5.5.2.1.13. Embedded (Deprecated)
|
Начиная с версии 6.8 платформы, компонент Embedded объявлен устаревшим (Deprecated). Используйте компонент Image для отображения графического содержимого и компонент BrowserFrame для встраивания веб-страниц. |
Компонент Embedded предназначен для вывода изображений и встраивания в экран произвольных веб-страниц.
XML-имя компонента: embedded
Компонент реализован для блоков Web Client и Desktop Client. В десктоп-клиенте поддерживается только вывод изображений.
Рассмотрим пример использования компонента для вывода изображения из файла, сохраненного в FileStorage.
<groupBox caption="Embedded" spacing="true"
height="250px" width="250px" expand="embedded">
<embedded id="embedded" width="100%"
align="MIDDLE_CENTER"/>
</groupBox>
@Inject
private Embedded embedded;
@Inject
private FileStorageService fileStorageService;
@Override
public void init(Map<String, Object> params) {
FileDescriptor imageFile = (FileDescriptor) params.get("imageFile");
byte[] bytes = null;
if (imageFile != null) {
try {
bytes = fileStorageService.loadFile(imageFile);
} catch (FileStorageException e) {
showNotification("Unable to load image file", NotificationType.HUMANIZED);
}
}
if (bytes != null) {
embedded.setSource(imageFile.getName(), new ByteArrayInputStream(bytes));
embedded.setType(Embedded.Type.IMAGE);
} else {
embedded.setVisible(false);
}
}
Компонент Embedded может отображать содержимое различных типов, которые по-разному отрисовываются в HTML. Тип содержимого можно задать методом setType(). Поддерживаются следующие типы:
В веб-клиенте компонент позволяет отображать файлы, находящиеся внутри каталога VAADIN. Например:
relativeSrc="VAADIN/themes/halo/my-logo.png"/>
или
embedded.setRelativeSource("VAADIN/themes/halo/my-logo.png")
Кроме того, можно определить каталог ресурсных файлов в свойстве приложения cuba.web.resourcesRoot, и указать для компонента Embedded имя файла внутри этого каталога с префиксом значения атрибута: file:// , url:// или theme://:
src="file://my-logo.png"/>
или
embedded.setSource("theme://branding/app-icon-menu.png");
Для встраивания в экран веб-клиента внешней веб-страницы необходимо передать компоненту URL:
try {
embedded.setSource(new URL("http://www.cuba-platform.com"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
Атрибуты embedded
align - height - id - relativeSrc - src - stylename - visible - width
Компонент FieldGroup предназначен для совместного отображения и редактирования нескольких атрибутов сущностей.
XML-имя компонента: fieldGroup
Компонент реализован для блоков Web Client и Desktop Client.
Пример описания группы полей в XML-дескрипторе экрана:
<dsContext>
<datasource id="orderDs"
class="com.sample.sales.entity.Order"
view="order-with-customer">
</datasource>
</dsContext>
<layout>
<fieldGroup id="orderFieldGroup" datasource="orderDs" width="250px">
<field property="date"/>
<field property="customer"/>
<field property="amount"/>
</fieldGroup>
</layout>
Здесь в элементе dsContext определен источник данных datasource, который содержит один экземпляр сущности Order. Для компонента fieldGroup в атрибуте datasource указывается используемый источник данных, а в элементах field - какие атрибуты сущности, содержащейся в источнике данных, необходимо отобразить.
Элементы fieldGroup:
<fieldGroup id="orderFieldGroup" datasource="orderDs" width="100%">
<column width="250px">
<field property="num"/>
<field property="date"/>
<field property="amount"/>
</column>
<column width="400px">
<field property="customer"/>
<field property="info"/>
</column>
</fieldGroup>
В данном случае поля будут расположены в две колонки, причем в первой колонке все поля будут шириной 250px, а во второй - 400px.
Атрибуты column:
Собственные настраиваемые поля можно определить внутри элемента field:
<fieldGroup>
<field id="demo">
<lookupField id="demoField" datasource="userDs" property="group"/>
</field>
</fieldGroup>
Атрибуты элемента field:
<fieldGroup datasource="productDs">
<column width="250px">
<field property="description" generator="generateDescriptionField"/>
</column>
</fieldGroup>
public Component generateDescriptionField(Datasource datasource, String fieldId) {
TextArea textArea = componentsFactory.createComponent(TextArea.class);
textArea.setRows(5);
textArea.setDatasource(datasource, fieldId);
return textArea;
}
Следующие атрибуты элемента field можно применять в зависимости от типа атрибута сущности, отображаемого полем:
Атрибуты fieldGroup:
Для элемента FieldGroup, полностью созданного программно, для этой цели используется метод setFieldFactory().
Методы интерфейса FieldGroup:
fieldGroup.addField(fieldGroup.createField("newField"));
Вместо явного вызова getFieldNN("id").getComponentNN() для получения ссылки на компонент поля в контроллере экрана можно использовать инжекцию. Для этого следует использовать аннотацию @Named с указанием идентификатора самого fieldGroup, и через точку - идентификатора поля.
Например, следующим образом в поле выбора связанной сущности можно добавить действие открытия экземпляра и убрать действие очистки поля:
<fieldGroup id="orderFieldGroup" datasource="orderDs">
<field property="date"/>
<field property="customer"/>
<field property="amount"/>
</fieldGroup>
@Named("orderFieldGroup.customer")
protected PickerField customerField;
@Override
public void init(Map<String, Object> params) {
customerField.addOpenAction();
customerField.removeAction(customerField.getAction(PickerField.ClearAction.NAME));
}
Для использования метода getComponent() или инжекции компонентов полей необходимо знать тип компонента, находящегося в поле. В следующей таблице приведено соответствие типов атрибутов сущностей и создаваемых для них компонентов:
|
Тип атрибута сущности |
Дополнительные условия |
Тип компонента поля |
|
Связанная сущность |
Задан атрибут optionsDatasource |
|
|
Перечисление (enum) |
||
|
string |
Задан атрибут mask |
|
|
Задан атрибут rows |
||
|
boolean |
||
|
date, dateTime |
||
|
time |
||
|
int, long, double, decimal |
Задан атрибут mask |
|
|
UUID |
MaskedField с hex-маской |
Экземпляр FieldConfig можно получить с помощью методов getField() или getFieldNN(), и затем вызвать метод setComponent(), как показано в следующем примере:
@Inject
protected FieldGroup fieldGroup;
@Inject
protected ComponentsFactory componentsFactory;
@Inject
private Datasource<User> userDs;
@Override
public void init(Map<String, Object> params) {
PasswordField passwordField = componentsFactory.createComponent(PasswordField.class);
passwordField.setDatasource(userDs, "password");
fieldGroup.getFieldNN("password").setComponent(passwordField);
}
Атрибуты fieldGroup
align - border - caption - captionAlignment - datasource - editable - enable - fieldFactoryBean - id - height - stylename - visible - width
Атрибуты column
Атрибуты field
caption - captionProperty - cols - custom - datasource - dateFormat - description - editable - enable - generator - id - link - linkInvoke - linkScreen - linkScreenOpenType - mask - maxLength - optionsDatasource - property - required - requiredMessage - resolution - rows - showSeconds - tabIndex - visible - width
Элементы field
column - field - inputPrompt - validator
API
addField - bind - createField - getComponent - removeField - setComponent - setFieldFactory
5.5.2.1.15. FileMultiUploadField
Компонент FileMultiUploadField позволяет пользователю загружать файлы на сервер. Компонент представляет собой кнопку, при нажатии на которую на экране отображается стандартное для операционной системы окно выбора файлов, в котором можно выбрать сразу несколько файлов для загрузки.
XML-имя компонента: multiUpload.
Компонент реализован для блоков Web Client и Desktop Client.
Рассмотрим пример использования компонента.
<multiUpload id="multiUploadField" caption="msg://upload"/>
@Inject
private FileMultiUploadField multiUploadField;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private DataSupplier dataSupplier;
@Override
public void init(Map<String, Object> params) {
multiUploadField.addQueueUploadCompleteListener(() -> {
for (Map.Entry<UUID, String> entry : multiUploadField.getUploadsMap().entrySet()) {
UUID fileId = entry.getKey();
String fileName = entry.getValue();
FileDescriptor fd = fileUploadingAPI.getFileDescriptor(fileId, fileName);
// save file to FileStorage
try {
fileUploadingAPI.putFileIntoStorage(fileId, fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error saving file to FileStorage", e);
}
// save file descriptor to database
dataSupplier.commit(fd);
}
showNotification("Uploaded files: " + multiUploadField.getUploadsMap().values(), NotificationType.HUMANIZED);
multiUploadField.clearUploads();
});
multiUploadField.addFileUploadErrorListener(event ->
showNotification("File upload error", NotificationType.HUMANIZED));
}
Компонент загружает выбранные файлы во временное хранилище клиентского уровня и вызывает слушатель, добавленный методом addQueueUploadCompleteListener(). В данном слушателе вызовом метода getUploadsMap() у компонента можно получить мэп идентификаторов файлов во временном хранилище на имена файлов. Далее для каждого элемента мэп создается соответствующий объект FileDescriptor путем вызова FileUploadingAPI.getFileDescriptor(). Объект com.haulmont.cuba.core.entity.FileDescriptor (не путать с java.io.FileDescriptor) является персистентной сущностью, которая однозначно идентифицирует загруженный файл и впоследствии используется для выгрузки файла из системы.
Ниже приведён список доступных слушателей для отслеживания процесса загрузки:
Метод FileUploadingAPI.putFileIntoStorage() используется для перемещения загружаемого файла из временного хранилища клиентского уровня в FileStorage. Параметрами этого метода являются идентификатор файла во временном хранилище и объект FileDescriptor.
После загрузки файла в FileStorage выполняется сохранение экземпляра FileDescriptor в базе данных посредством вызова DataSupplier.commit(). Возвращаемый этим методом сохраненный экземпляр может быть установлен в атрибут какой-либо сущности предметной области, связанной с данным файлом. В данном же случае FileDescriptor просто сохраняется в базе данных. Соответствующий файл будет доступен через экран Administration → External Files.
После обработки файлов необходимо очистить список файлов вызовом clearUploads() на случай повторной загрузки.
Значением атрибута должен быть список масок, разделенных запятыми. Например: *.jpg,*.png.
<multiUpload id="multiUploadField" fileSizeLimit="200000"/>
Значением атрибута должен быть набор строк, в котором каждая строка - это допустимое расширение с лидирующей точкой. Например:
uploadField.setPermittedExtensions(Sets.newHashSet(".png", ".jpg"));
В разделе Загрузка и вывод изображений приведен более сложный пример работы с загруженными файлами.
Атрибуты multiUpload
accept - align - caption - description - dropZone - fileSizeLimit - enable - height - icon - id - pasteZone - permittedExtensions - stylename - tabIndex - visible - width
Слушатели multiUpload
FileUploadErrorListener - FileUploadFinishListener - FileUploadStartListener - QueueUploadCompleteListener
Компонент FileUploadField позволяет пользователю загружать файлы на сервер. Компонент может содержать заголовок, ссылку на загруженный файл и две кнопки: для загрузки файла и для очистки. При нажатии на кнопку загрузки на экране отображается стандартное для операционной системы окно, в котором можно выбрать один файл. Чтобы дать пользователю возможность загружать сразу несколько файлов, используйте компонент FileMultiUploadField.
XML-имя компонента: upload.
Компонент реализован для блоков Web Client и Desktop Client.
<upload fileStoragePutMode="IMMEDIATE"
datasource="personDs"
property="photo"/>
<upload id="uploadField"
fileStoragePutMode="MANUAL"/>
@Inject
private FileUploadField uploadField;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private DataSupplier dataSupplier;
@Override
public void init(Map<String, Object> params) {
uploadField.addFileUploadSucceedListener(event -> {
// here you can get the file uploaded to the temporary storage if you need it
File file = fileUploadingAPI.getFile(uploadField.getFileId());
if (file != null) {
showNotification("File is uploaded to temporary storage at " + file.getAbsolutePath());
}
// normally you would want to save the file to the file storage of the middle tier
FileDescriptor fd = uploadField.getFileDescriptor();
try {
// save file to FileStorage
fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error saving file to FileStorage", e);
}
// save file descriptor to database
dataSupplier.commit(fd);
showNotification("Uploaded file: " + uploadField.getFileName());
});
uploadField.addFileUploadErrorListener(event ->
showNotification("File upload error"));
}
Компонент загружает файл во временное хранилище клиентского уровня и вызывает слушатель, добавленный методом addFileUploadSucceedListener(). В этом слушателе у компонента запрашивается объект FileDescriptor, соответствующий загруженному файлу. Объект com.haulmont.cuba.core.entity.FileDescriptor является персистентной сущностью, которая однозначно идентифицирует загруженный файл и впоследствии используется для выгрузки файла из системы.
Метод FileUploadingAPI.putFileIntoStorage() используется для перемещения загружаемого файла из временного хранилища клиентского уровня в FileStorage. Параметрами этого метода являются идентификатор файла во временном хранилище и объект FileDescriptor. Оба эти параметра предоставляет FileUploadField.
После загрузки файла в FileStorage выполняется сохранение экземпляра FileDescriptor в базе данных посредством вызова DataSupplier.commit(). Возвращаемый этим методом сохраненный экземпляр может быть установлен в атрибут какой-либо сущности предметной области, связанной с данным файлом. В данном же случае FileDescriptor просто сохраняется в базе данных. Соответствующий файл будет доступен через экран Administration → External Files.
Слушатель, добавленный методом addFileUploadErrorListener(), вызывается в случае ошибки загрузки файла во временное хранилище клиентского уровня.
Ниже приведён полный список доступных слушателей для отслеживания процесса загрузки:
Атрибуты fileUploadField:
Режим IMMEDIATE выбирается по умолчанию, когда FileUploadField используется внутри FieldGroup. В противном случае, по умолчанию выбирается MANUAL.
Значением атрибута должен быть список масок, разделенных запятыми. Например: *.jpg,*.png.
<upload id="uploadField" fileSizeLimit="2000"/>
Значением атрибута должен быть набор расширений с лидирующими точками, разделенных запятыми. Например:
uploadField.setPermittedExtensions(Sets.newHashSet(".png", ".jpg"));
<layout spacing="true"
width="100%">
<vbox id="dropZone"
height="AUTO"
spacing="true">
<textField id="textField"
caption="Title"
width="100%"/>
<textArea id="textArea"
caption="Description"
width="100%"
rows="5"/>
<checkBox caption="Is reference document"
width="100%"/>
<upload id="upload"
dropZone="dropZone"
showClearButton="true"
showFileName="true"/>
</vbox>
<hbox spacing="true">
<button caption="mainMsg://actions.Apply"/>
<button caption="mainMsg://actions.Cancel"/>
</hbox>
</layout>
Чтобы сделать область dropZone статической и отображать её постоянно, необходимо назначить её контейнеру готовый стиль dropzone-container. В этом случае контейнер необходимо оставить пустым, поместив в него только текстовый компонент label:
<layout spacing="true"
width="100%">
<textField id="textField"
caption="Title"
width="100%"/>
<checkBox caption="Is reference document"
width="100%"/>
<upload id="upload"
dropZone="dropZone"
showClearButton="true"
showFileName="true"/>
<vbox id="dropZone"
height="150px"
spacing="true"
stylename="dropzone-container">
<label stylename="dropzone-description"
value="Drop file here"
align="MIDDLE_CENTER"/>
</vbox>
<hbox spacing="true">
<button caption="mainMsg://actions.Apply"/>
<button caption="mainMsg://actions.Cancel"/>
</hbox>
</layout>
<upload id="uploadField"
pasteZone="vboxId"
showClearButton="true"
showFileName="true"/>
В разделе Загрузка и вывод изображений приведен более сложный пример работы с загруженными файлами.
Атрибуты upload
accept - align - caption - clearButtonCaption - clearButtonDescription - clearButtonIcon - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - dropZone - editable - enable - fileSizeLimit - fileStoragePutMode - height - icon - id - pasteZone - permittedExtensions - property - showClearButton - showFileName - stylename - tabIndex - uploadButtonCaption - uploadButtonDescription - uploadButtonIcon - visible - width
API
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
Слушатели upload
AfterValueClearListener - BeforeValueClearListener - FileUploadErrorListener - FileUploadFinishListener - FileUploadStartListener - FileUploadSucceedListener
В этом разделе:
Компонент Filter − универсальное средство фильтрации списков сущностей, извлекаемых из базы данных для отображения в табличном виде. Компонент позволяет производить быструю фильтрацию данных по произвольному набору условий, а также создавать фильтры для многократного использования.
Filter должен быть связан с источником данных collectionDatasource содержащим запрос на JPQL. Принцип действия фильтра основан на модификации этого запроса в соответствии с критериями, заданными пользователем. Таким образом фильтрация осуществляется на уровне БД при выполнении транслированного из JPQL в SQL запроса, и на Middleware и клиентский уровень загружаются только отобранные данные.
Типичный фильтр выглядит следующим образом:
По умолчанию, компонент находится в режиме быстрой фильтрации. Это означает, что пользователь может добавить набор условий для однократного поиска данных. После закрытия экрана просмотра экземпляров сущности условия будут удалены.
Для того чтобы создать быстрый фильтр, нажмите на ссылку Add search condition (Добавить условие поиска). Отобразится экран выбора условий:
Рассмотрим возможные типы условий:
Выбранные условия отображаются в верхней части панель фильтра. Рядом с каждым условием находится кнопка
, позволяющая удалить их из набора.
Быстрый фильтр можно сохранить для повторного использования в дальнейшем. Для этого нажмите на кнопку настроек фильтра и выберите Save/Save as (Сохранить/Сохранить как). Во всплывающем окне задайте имя нового фильтра:
Фильтр будет сохранен в выпадающем меню кнопки Search (Поиск).
Пункт меню Reset filter (Сбросить фильтр) позволяет сбросить все текущие условия поиска.
Кнопка настроек фильтра содержит выпадающий список опций для управления фильтром:
Опция Edit открывает редактор фильтра, который дает возможность расширенной настройки текущего фильтра:
Название фильтра указывается в поле Filter name (Имя фильтра). Это имя будет отображаться в списке доступных фильтров для текущего экрана.
Фильтр можно сделать global (то есть доступным для всех пользователей) с помощью установки флажка Available for all users (Общий) для всех пользователей и global default с помощью флажка Global default. Для этих операций пользователю требуется специфическое разрешение CUBA > Фильтр > Создание/изменение глобальных фильтров. Если фильтр помечен как global default, то он будет автоматически выбран при открытии экрана пользователями. Каждый пользователь может установить свой собственный фильтр по умолчанию с помощью флажка Default (По умолчанию). Эта настройка имеет приоритет над global default.
В дереве содержатся условия фильтра. Условия можно добавлять с помощью кнопки Add (Добавить) менять местами при помощи кнопок
/
или удалять с помощью кнопки Remove (Удалить).
Группировку условий по И или ИЛИ можно добавить с помощью соответствующих кнопок. Все добавленные на верхний уровень (то есть без явной группировки) условия объединяются по И.
При выборе условия в дереве в правой части редактора открывается список его свойств.
С помощью соответствующих флажков можно сделать выбранное в таблице условие скрытым или обязательным для заполнения. Параметр скрытого условия не отображается пользователю, поэтому он должны быть введен во время редактирования фильтра.
Свойство Width позволяет задать ширину поля ввода параметра для текущего условия. По умолчанию, условия на панели фильтров отображаются в три колонки. Ширина поля равняется количеству колонок, которое оно может занять (1, 2 или 3).
Значение параметра текущего условия по умолчанию можно задать в поле Default value (Значение по умолчанию).
Специальный заголовок условия фильтрации можно задать в поле Caption (Заголовок).
Поле Operation позволяет выбрать оператор поиска. Список доступных операторов зависит от типа атрибута.
При поиске по атрибуту сущности с типом DateTime и без аннотации @IgnoreUserTimeZone по умолчанию будет учитываться часовой пояс текущего пользователя. Чтобы учитывать часовой пояс для атрибута с типом Date, необходимо установить флаг Use time zone в редакторе нового условия.
XML-имя компонента: filter.
Компонент реализован для блоков Web Client и Desktop Client.
Пример объявления компонента в XML-дескрипторе экрана:
<dsContext>
<collectionDatasource id="carsDs" class="com.company.sample.entity.Car" view="carBrowse">
<query>
select c from ref$Car c order by c.createTs
</query>
</collectionDatasource>
</dsContext>
<layout>
<filter id="carsFilter" datasource="carsDs">
<properties include=".*"/>
</filter>
<table id="carsTable" width="100%">
<columns>
<column id="vin"/>
<column id="model.name"/>
<column id="colour.name"/>
</columns>
<rows datasource="carsDs"/>
</table>
</layout>
Здесь в элементе dsContext определен источник данных collectionDatasource, который выбирает экземпляры сущности Car с помощью JPQL запроса. Для компонента filter в его атрибуте datasource указан фильтруемый источник данных. Данные отображаются компонентом Table, связанным с этим же источником.
Элемент filter может содержать вложенные элементы. Все они описывают условия, доступные пользователю для выбора в диалоге добавления условий:
Пример:
<filter id="filter"
applyTo="ordersTable"
datasource="ordersDs">
<properties include=".*"
exclude="(amount)|(id)"
excludeProperties="version,createTs,createdBy,updateTs,updatedBy,deleteTs,deletedBy"
excludeRecursively="true"/>
</filter>
Чтобы программно исключить атрибуты из фильтра, используйте метод setPropertiesFilterPredicate() компонента Filter:
filter.setPropertiesFilterPredicate(metaPropertyPath ->
!metaPropertyPath.getMetaProperty().getName().equals("createTs"));
При использовании элемента properties автоматически игнорируются следующие атрибуты сущности:
<filter id="transactionsFilter" datasource="transactionDs" applyTo="table">
<properties include=".*" exclude="(masterTransaction)|(authCode)"/>
<property name="creditCard.maskedPan" caption="msg://EmbeddedCreditCard.maskedPan"/>
<property name="creditCard.startDate" caption="msg://EmbeddedCreditCard.startDate"/>
</filter>
Если в атрибуте name указан путь (через ".") по графу сущностей, то атрибут caption является обязательным.
Например, предположим, что сущность Car имеет ссылку на сущность Model. Тогда список возможных значений параметра может быть ограничен только моделями Audi:
<filter id="carsFilter" datasource="carsDs">
<property name="model" paramWhere="{E}.manufacturer = 'Audi'"/>
</filter>
В выражении JPQL можно использовать параметры экрана, атрибуты сессии, а также компоненты экрана, в том числе отображающие другие параметры. Правила задания параметров запроса описаны в Запросы в CollectionDatasourceImpl.
Пример использования параметра сессии и параметра экрана:
{E}.createdBy = :session$userLogin and {E}.name like :param$groupName
Используя paramWhere можно вводить зависимости между параметрами. Например, предположим, что Manufacturer является отдельной сущностью. То есть Car ссылается на Model, которая в свою очередь ссылается на Manufacturer. Тогда для фильтра по Car можно создать два условия: первое для выбора Manufacturer и второе для выбора Model. Чтобы ограничить список моделей выбранным перед этим производителем, добавьте в выражение paramWhere параметр:
{E}.manufacturer.id = :component$filter.model_manufacturer90062
Здесь параметр ссылается на компонент, отображающий параметр Manufacturer. Имя компонента, отображающего параметр условия, можно узнать, вызвав контекстное меню на строке таблицы условий в редакторе фильтра:
Значение условия может содержать спецсимволы, например "%" или "_" для оператора "like". Если вам нужно экранировать эти символы, добавьте в условие escape '<char>', например:
{E}.name like ? escape '\'
Тогда если в значении параметра условия будет передано foo\%, поиск будет интерпретировать "%" как символ в имени а не как спецсимвол.
Пример фильтра с произвольными условиями:
<filter id="carsFilter" datasource="carsDs">
<properties include=".*"/>
<custom name="vin" paramClass="java.lang.String" caption="msg://vin">
{E}.vin like ?
</custom>
<custom name="colour" paramClass="com.company.sample.entity.Colour" caption="msg://colour"
inExpr="true">
({E}.colour.id in (?))
</custom>
<custom name="repair" paramClass="java.lang.String" caption="msg://repair"
join="join {E}.repairs cr">
cr.description like ?
</custom>
<custom name="updateTs" caption="msg://updateTs">
@between({E}.updateTs, now-1, now+1, day)
</custom>
</filter>
Созданные custom условия отображаются в секции Специальные условия диалога добавления условий:
Атрибуты элемента custom:
Например, предположим что сущность Car имеет атрибут repairs, который представляет собой коллекцию экземпляров связанной сущности Repair. Тогда для фильтрации Car по атрибуту description сущности Repair можно написать следующее условие:
<filter id="carsFilter" datasource="carsDs">
<custom name="repair"
caption="msg://repair"
paramClass="java.lang.String"
join="join {E}.repairs cr">
cr.description like ?
</custom>
</filter>
При использовании такого условия исходный запрос источника данных:
select c from sample$Car c order by c.createTs
будет трансформирован в следующий:
select c from sample$Car c join c.repairs cr
where (cr.description like ?)
order by c.createTs
Атрибуты filter:
Данный атрибут имеет приоритет над свойством приложения cuba.gui.genericFilterManualApplyRequired.
Если значение этого атрибута равно false, то фильтр не будет отображать поле Show rows. Количество записей в источнике данных (и соответственно, показываемых таблицей) будет ограничено только параметром MaxFetchUI механизма статистики сущностей, по умолчанию - 10000.
Если данный атрибут не указан, или равен true, то поле Show rows отображается, если у пользователя также есть специфическое разрешение cuba.gui.filter.maxResults. Если разрешение cuba.gui.filter.maxResults отсутствует, то фильтр будет принудительно отбирать только первые N строк без возможности пользователя отключить это или указать другое N. Число N определяется параметрами FetchUI, DefaultFetchUI, получаемыми из механизма статистики сущностей.
На рисунке далее показан вид фильтра со значением атрибута useMaxResults="true", запретом специфического разрешения cuba.gui.filter.maxResults и параметром DefaultFetchUI=2
Методы интерфейса Filter:
Слушатели компонента Filter:
Атрибуты filter
applyTo - caption - columnsCount - datasource - defaultMode - editable - enable - folderActionsEnabled - id - manualApplyRequired - margin - modeSwitchVisible - settingsEnabled - stylename - textMaxResults - useMaxResults - visible - width
Элементы filter
custom - properties - property
Атрибуты properties
exclude - excludeProperties - include
Атрибуты property
caption - name - paramView - paramWhere
Атрибуты custom
caption - name - inExpr - join - paramClass - paramView - paramWhere
API
addExpandedStateChangeListener - addFilterEntityChangeListener - applySettings - getMargin - saveSettings - setMargin
Информация о том, как настраивать специфические разрешения, приведена в руководстве Подсистема безопасности.
Внешние параметры для управления фильтрами
Следующие свойства приложения влияют на поведение фильтров:
При вызове экрана можно указать, какой фильтр и с какими параметрами должен быть применен сразу после открытия экрана. Для этого фильтр должен быть заранее создан, сохранен в базе данных, и соответствующая запись в таблице SEC_FILTER должна иметь заполненное поле CODE. Параметры вызова экрана задаются в конфигурационном файле web-menu.xml.
Чтобы сохранить фильтр в базе данных, необходимо добавить скрипт вставки фильтра в скрипт 30.create-db.sql сущности. Для генерации скрипта найдите фильтр в справочнике Entity Inspector меню Administration, в контекстном меню выберите System Information, нажмите на кнопку Script for insert и скопируйте текст скрипта.
Теперь можно добавить скрипт к экрану. Для указания кода фильтра в экран следует передать параметр с именем, равным идентификатору компонента фильтра в данном экране. Значением параметра должен быть код фильтра, который нужно установить и применить.
Для установки значений параметров фильтра в экран нужно передать параметры с именами, равными именам параметров, и значения в виде строк.
Пример описателя пункта главного меню, устанавливающего в открываемом экране sample$Car.browse в компоненте carsFilter фильтр с кодом FilterByVIN, с подстановкой в параметр условия component$carsFilter.vin79216 значения TMA:
<item id="sample$Car.browse">
<param name="carsFilter" value="FilterByVIN"/>
<param name="component$carsFilter.vin79216" value="TMA"/>
</item>
Следует отметить, что фильтр с установленным полем CODE обладает особыми свойствами:
Последовательное наложение фильтров
При включенном свойстве приложения cuba.allowQueryFromSelected в пользовательском интерфейсе компонента можно закреплять последний примененный фильтр и текущие результаты фильтрации. После этого можно выбрать другой фильтр или параметры и применить их на уже выбранных записях.
Данный подход позволяет решить две проблемы:
Чтобы применить этот механизм в пользовательском интерфейсе, выберите и примените один из фильтров. Затем нажмите на кнопку настроек фильтра и выберите Pin applied (Закрепить). Фильтр закрепится в верхней части панели фильтров. Далее можно применить к выбранным записям другой фильтр. Так последовательно можно накладывать друг на друга любое количество фильтров. Также фильтры можно удалять последовательно с помощью кнопки
.
Механизм последовательного наложения фильтров основан на возможности DataManager выполнять последовательные запросы.
API для работы с параметрами фильтра
Интерфейс Filter предоставляет методы для установки и чтения значений параметра фильтра из кода контроллера экрана:
paramName - имя параметра фильтра. Имя параметра фильтра является составной частью имени компонента, отображающего значение параметра фильтра. Как получить имя компонента рассматривалось выше. Имя параметра - это часть имени компонента, находящаяся после последней точки. Например, если имя компонента component$filter.model_manufacturer90062, то имя параметра фильтра model_manufacturer90062.
Обратите внимание, что в методе init() контроллера экрана данные методы использовать нельзя, т.к. в этот момент фильтр еще не проинициализирован. Вы можете работать с параметрами фильтра в методе ready().
Режим полнотекстового поиска в фильтре
Если источник данных фильтра содержит сущности, индексируемые системой полнотекстового поиска (см. Платформа CUBA. Полнотекстовый поиск), то в фильтре становится доступным режим полнотекстового поиска. Чтобы переключиться в него, используйте флажок Full-Text Search ("Полнотекстовый поиск").
В этом режиме фильтр имеет поля для ввода критериев поиска, и поиск производится по индексируемым подсистемой FTS полям сущности.
Если таблица указана в атрибуте applyTo, то при наведении указателя мыши на строку таблицы во всплывающем окне будет написано, в каких полях сущности было найдено условие поиска.
Для скрытия переключателя режима фильтра установите значение false атрибуту фильтра modeSwitchVisible.
Если необходимо, чтобы фильтр по умолчанию открывался в режиме полнотекстового поиска, установите значение fts атрибуту defaultMode.
Полнотекстовый поиск может использоваться совместно с любым количеством условий универсального фильтра:
Выбрать условие FTS condition можно в окне выбора условий фильтра.
Компонент GroupTable - это таблица с возможностью динамической группировки по любому полю. Для того чтобы сгруппировать таблицу по какой-либо колонке, нужно в заголовке таблицы перетащить эту колонку в позицию слева от элемента
. Сгруппированные значения можно разворачивать и сворачивать с помощью кнопок
/
.
XML-имя компонента: groupTable.
Компонент реализован только для блока Web Client. В Desktop Client ведет себя как обычная таблица.
Для GroupTable в атрибуте datasource элемента rows должен быть указан groupDatasource. В противном случае группировка работать не будет.
Пример использования:
<dsContext>
<groupDatasource id="ordersDs" class="com.sample.sales.entity.Order"
view="order-with-customer">
<query>
select o from sales$Order o order by o.date
</query>
</groupDatasource>
</dsContext>
<layout>
<groupTable id="ordersTable" width="100%">
<columns>
<group>
<column id="date"/>
</group>
<column id="customer.name"/>
<column id="amount"/>
</columns>
<rows datasource="ordersDs"/>
</groupTable>
group − необязательный элемент, может в единственном экземпляре находиться внутри columns. Содержит набор элементов column, по которым будет выполняться первоначальная группировка при открытии экрана.
Элемент column может содержать атрибут groupAllowed с булевым значением. С помощью этого атрибута можно запретить пользователю группировать таблицу по данной колонке.
При включенном атрибуте aggregatable таблица отображает результаты агрегации по всем строкам в дополнительной строке вверху, а также результаты агрегации по группам. Отображение агрегации по всем строкам можно отключить, установив false в атрибуте showTotalAggregation.
При включенном атрибуте multiselect клик по строке группировки с зажатой клавишей Ctrl разворачивает группу (если была свёрнута) и применяет выделение ко всем строкам этой группы. При этом, если вся группа выделена, Ctrl+click не снимает выделение со всей группы. Вы по-прежнему можете снять выделение с отдельных строк, пользуясь стандартным поведением клавиши Ctrl.
Методы интерфейса GroupTable:
В примере ниже таблица будет сгруппирована сначала по колонке department, а затем - по колонке city:
groupTable.groupByColumns("department", "city");
Следующий пример снимет группировку по department, однако группировка по колонке city из предыдущего примера будет сохранена:
groupTable.ungroupByColumns("department");
В остальном функциональность GroupTable аналогична простой таблице Table.
Атрибуты groupTable
align - aggregatable - aggregationStyle - columnControlVisible - contextMenuEnabled - editable - enable - height - id - multiLineCells - multiselect - presentations - reorderingAllowed - settingsEnabled - showTotalAggregation - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
Элементы groupTable
actions - buttonsPanel - columns - rows - rowsCount
Элементы columns
Атрибуты column
align - caption - captionProperty - collapsed - dateFormat - editable - groupAllowed - id - link - linkInvoke - linkScreen - linkScreenOpenType - maxTextLength - optionsDatasource - resolution - sortable - visible - width
Элементы column
Атрибуты rows
API
addColumnCollapseListener - groupByColumns - setClickListener - ungroup - ungroupByColumns
Компонент Image предназначен для отображения графического содержимого из различных источников. Компонент можно привязать к источнику данных или сконфигурировать программно.
XML имя компонента: image.
Компонент Image может отображать значение атрибута сущности с типом FileDescriptor или byte[]. Для этого используются атрибуты datasource и property, например:
<image id="image" datasource="employeeDs" property="avatar"/>
В данном случае компонент отображает атрибут avatar сущности Employee, находящейся в источнике данных employeeDs.
Помимо источников данных, компонент Image может использовать в качестве источника содержимого различные типы ресурсов. Тип ресурса можно указать декларативно с помощью элементов image, перечисленных ниже:
<image>
<classpath path="com/company/sample/web/screens/myPic.jpg"/>
</image>
<image>
<file path="D:\sample\modules\web\web\VAADIN\images\myImage.jpg"/>
</image>
<image>
<relativePath path="VAADIN/images/myImage.jpg"/>
</image>
<image>
<theme path="com.company.sample/myPic.jpg"/>
</image>
<image>
<url url="https://www.cuba-platform.com/sites/all/themes/cuba_adaptive/img/lori.png"/>
</image>
Атрибуты image:
<image id="image" alternateText="logo"/>
Параметры ресурсов image:
<image>
<file bufferSize="1024" path="C:/img.png"/>
</image>
<image>
<file cacheTime="2400" path="C:/img.png"/>
</image>
<image>
<url url="https://avatars3.githubusercontent.com/u/17548514?v=4&s=200"
mimeType="image/png"/>
</image>
Методы интерфейса Image:
Программное указание источника данных позволяет, к примеру, отображать изображения в ячейках таблицы:
frameworksTable.addGeneratedColumn("image", entity -> {
Image image = componentsFactory.createComponent(Image.class);
image.setDatasource(frameworksTable.getItemDatasource(entity), "image");
image.setHeight("100px");
return image;
});
Image image = componentsFactory.createComponent(Image.class);
image.setSource(ThemeResource.class)
.setPath("images/image.png");
или
image.setSource(StreamResource.class)
.setStreamSupplier(() -> new FileDataProvider(fileDescriptor).provide())
.setBufferSize(1024);
Можно использовать следующие типы ресурсов, реализующие интерфейс Resource, или расширить их:
FileDescriptorResource resource = image.createResource(FileDescriptorResource.class)
.setFileDescriptor(avatar);
image.setSource(resource);
image.addClickListener(event -> {
if (event.isDoubleClick())
showNotification("Double clicked");
});
Атрибуты image
align - alternateText - caption - colspan - datasource - description - editable - enable - height - icon - id - property - required - requiredMessage - responsive - rowspan - scaleMode - stylename - stylename - visible - width
Атрибуты ресурсов image
bufferSize - cacheTime - mimeType
Элементы image
classpath - file - relativePath - theme - url
API
addClickListener - addSourceChangeListener - createResource - setScaleMode - setDatasource - setSource
Надпись (Label) − текстовый компонент, отображающий статический текст либо значение атрибута сущности.
XML-имя компонента: label
Компонент Label реализован для блоков Web Client и Desktop Client.
Пример задания надписи с текстом, взятым из пакета локализованных сообщений:
<label value="msg://orders"/>
Атрибут value предназначен для задания текста надписи.
В веб-клиенте текст, содержащийся в атрибуте value, будет разбит на несколько строк, если по длине он превысит значение атрибута width. Поэтому для отображения многострочной надписи, достаточно указать абсолютное значение атрибута width. Если текст надписи слишком длинный, а значение атрибута width не определено, то текст будет урезан.
<label value="Label, which should be split into multiple lines"
width="200px"/>
Параметры надписи можно задать в контроллере экрана. Для этого необходимо задать компоненту идентификатор, по которому получить ссылку на него в контроллере:
<label id="dynamicLabel"/>
@Inject
private Label dynamicLabel;
public void init(Map<String, Object> params) {
dynamicLabel.setValue("Some value");
}
Компонент Label может отображать значение атрибута сущности. Для этого используются атрибуты datasource и property. Например:
<dsContext>
<datasource id="customerDs" class="com.sample.sales.entity.Customer" view="_local"/>
</dsContext>
<layout>
<label datasource="customerDs" property="name"/>
В данном случае компонент отображает атрибут name сущности Customer, находящейся в источнике данных customerDs.
Атрибут htmlEnabled указывает, каким образом будет рассматриваться значение атрибута value: при htmlEnabled="true" как HTML-код, иначе как строка. Обратите внимание, что не все HTML-теги поддерживаются в десктоп-реализации экрана.
В веб-клиенте с темой, основанной на Halo, к компоненту Label можно применить заданные стили. Стили задаются в XML-дексрипторе или контроллере экрана с помощью атрибута stylename:
<label value="Label to be styled"
stylename="colored"/>
Чтобы применить стиль программно, выберите одну из констант класса HaloTheme с префиксом компонента LABEL_:
label.setStyleName(HaloTheme.LABEL_COLORED);
Атрибуты label
align - datasource - enable - height - htmlEnabled - icon - id - property - stylename - value - visible - width
Элементы label
Предопределенные стили label
bold - colored - failure - h1 - h2 - h3 - h4 - huge - large - light - no-margin - small - spinner - success - tiny
API
Ссылка (Link) − компонент-гиперссылка, позволяющая открывать внешние веб-ресурсы единообразно для веб и десктоп клиента.
XML-имя компонента: link
Пример XML-описания компонента link:
<link caption="Link" url="https://www.cuba-platform.com" target="_blank"/>
Атрибуты link:
Значение по умолчанию "noopener noreferrer".
Атрибуты link
align - caption - description - enable - icon - id - rel - stylename - url - target - visible - width
Кнопка-ссылка (LinkButton) − кнопка, выглядящая как гиперссылка.
XML-имя компонента: linkButton
Компонент кнопки-ссылки реализован для блоков Web Client и Desktop Client.
Кнопка-ссылка может содержать текст или значок (или и то и другое). На рисунке ниже отражены разные виды кнопок.
Кнопка-ссылка отличается от обычной кнопки Button только своим внешним видом. Все свойства и поведение идентичны описанным для Button.
Пример XML-описания кнопки-ссылки, вызывающей метод someMethod() контроллера, с надписью (атрибут caption), всплывающей подсказкой (атрибут description) и значком (атрибут icon):
<linkButton id="linkButton"
caption="msg://linkButton"
description="Press me"
icon="SAVE"
invoke="someMethod"/>
Атрибуты linkButton
action - align - caption - description - enable - icon - id - invoke - stylename - visible - width
Компонент для выбора значения из выпадающего списка. Выпадающий список реализует фильтрацию значений по мере ввода пользователя и постраничный вывод доступных значений.
XML-имя компонента: lookupField.
Компонент LookupField реализован для блоков Web Client и Desktop Client.
<dsContext>
<datasource id="roleDs" class="com.haulmont.cuba.security.entity.Role" view="_local"/>
</dsContext>
<layout>
<lookupField datasource="roleDs" property="type"/>
Как видно из примера, в экране описывается источник данных roleDs для сущности Role. В компоненте lookupField в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено. В данном случае атрибут является перечислением, и в выпадающем списке будут отображены локализованные названия всех значений этого перечисления.
<dsContext>
<datasource id="carDs" class="com.company.sample.entity.Car" view="_local"/>
<collectionDatasource id="coloursDs" class="com.company.sample.entity.Colour" view="_local">
<query>select c from sample$Colour c</query>
</collectionDatasource>
</dsContext>
<layout>
<lookupField datasource="carDs" property="colour" optionsDatasource="coloursDs"/>
В данном случае компонент отобразит отобразит имена экземпляров сущности Colour, находящихся в источнике данных colorsDs, а выбранное значение подставится в атрибут colour сущности Car, находящейся в источнике данных carDs.
С помощью атрибута captionProperty можно указать, какой атрибут сущности использовать вместо имени экземпляра для строковых названий опций.
<lookupField id="numberOfSeatsField" datasource="modelDs" property="numberOfSeats"/>
Затем инжектируем компонент в контроллер и в методе init() задаем ему список опций:
@Inject
protected LookupField numberOfSeatsField;
@Override
public void init(Map<String, Object> params) {
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(4);
list.add(5);
list.add(7);
numberOfSeatsField.setOptionsList(list);
}
В выпадающем списке компонента отобразятся числа 2, 4, 5, 7. Выбранное число подставится в атрибут numberOfSeats сущности, находящейся в источнике данных modelDs.
@Inject
protected LookupField numberOfSeatsField;
@Override
public void init(Map<String, Object> params) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("two", 2);
map.put("four", 4);
map.put("five", 5);
map.put("seven", 7);
numberOfSeatsField.setOptionsMap(map);
}
В выпадающем списке компонента отобразятся строки two, four, five, seven. Однако значением компонента будет число, соответствующее выбранной строке. Оно и подставится в атрибут numberOfSeats сущности, находящейся в источнике данных modelDs.
lookupField.setOptionsStyleProvider((field, item) -> {
User user = (User) item;
switch (user.getGroup().getName()) {
case "Company":
return "company";
case "Premium":
return "premium";
default:
return "company";
}
});
lookupField.setOptionIconProvider(new LookupField.OptionIconProvider<Customer>(){
@Override
public String getItemIcon(Customer c){
if(c.getType()== LegalStatus.LEGAL)
return"icons/icon-office.png";
return"icons/icon-user.png";
}
});
|
При использовании значков в формате SVG необходимо явно указывать их размеры, чтобы избежать наложения: |
<svg version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
style="enable-background:new 0 0 55 55;"
viewBox="0 0 55 55"
height="25px"
width="25px">
<lookupField datasource="carDs" property="colour" optionsDatasource="coloursDs" nullName="(none)"/>
В данном случае вместо пустой строки отобразится строка (none), при выборе которой в связанный атрибут сущности подставится значение null.
При программном задании списка опций методом setOptionsList() можно одну из опций передать в метод setNullOption(). Тогда при ее выборе пользователем значением компонента будет null.
Фильтрация опций LookupField:
BiFunction<String, String, Boolean> predicate = String::contains;
lookupField.setFilterPredicate((itemCaption, searchString) ->
predicate.apply(itemCaption.toLowerCase(), searchString));
Функциональный интерфейс FilterPredicate содержит метод test, который позволяет реализовать более сложную логику фильтрации опций, например, игнорировать специальные символы или надстрочные знаки:
lookupField.setFilterPredicate((itemCaption, searchString) ->
StringUtils.replaceChars(itemCaption, "ÉÈËÏÎ", "EEEII")
.toLowerCase()
.contains(searchString));
@Inject
private Metadata metadata;
@Inject
protected LookupField colourField;
@Inject
protected CollectionDatasource<Colour, UUID> coloursDs;
@Override
public void init(Map<String, Object> params) {
colourField.setNewOptionAllowed(true);
colourField.setNewOptionHandler(new LookupField.NewOptionHandler() {
@Override
public void addNewOption(String caption) {
Colour colour = metadata.create(Colour.class);
colour.setName(caption);
coloursDs.addItem(colour);
colourField.setValue(colour);
}
});
}
Обработчик NewOptionHandler вызывается, если пользователь ввел некоторое значение, не совпадающее ни с одной из опций, и нажал Enter. В данном случае в обработчике создается новый экземпляр сущности Colour, его атрибут name устанавливается в значение, введенное пользователем, этот экземпляр добавляется в источник данных опций и выбирается в компоненте.
Вместо имплементации интерфейса LookupField.NewOptionHandler для обработки ввода пользователя можно использовать XML-атрибут newOptionHandler с указанным в нем методом контроллера. Данный метод должен иметь два параметра - первый типа LookupField, второй типа String. В них будут переданы соответственно экзампляр компонента и введенное пользователем значение. Атрибут newOptionAllowed используется вместо метода setNewOptionAllowed() для того, чтобы разрешить добавление новых опций.
<lookupField id="lookupField"
stylename="borderless"/>
Чтобы применить стиль программно, выберите одну из констант класса HaloTheme с префиксом компонента LOOKUPFIELD_:
lookupField.setStyleName(HaloTheme.LOOKUPFIELD_BORDERLESS);
Стили компонента LookupField:
Атрибуты lookupField
align - caption - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - filterMode - height - icon - id - inputPrompt - newOptionAllowed - newOptionHandler - nullName - nullOptionVisible - optionsDatasource - optionsEnum - pageLength - property - required - requiredMessage - stylename - tabIndex - textInputAllowed - visible - width
Элементы lookupField
Предопределенные стили lookupField
align-right - align-center - borderless - huge - large - small - tiny
API
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler - setFilterPredicate - setOptionsEnum - setOptionsList - setOptionsMap - setOptionsStyleProvider
Компонент LookupPickerField позволяет отображать экземпляр сущности в текстовом поле, выбирать экземпляр в выпадающем списке и выполнять действия нажатием на кнопки справа.
XML-имя компонента: lookupPickerField.
Компонент реализован для блоков Web Client и Desktop Client.
LookupPickerField является по сути гибридом LookupField и PickerField, поэтому все описанное для этих интерфейсов верно и для него. Исключением является список действий по умолчанию, добавляемых при определении компонента в XML: для LookupPickerField это действия lookup
и open
.
Пример использования LookupPickerField для выбора значения ссылочного атрибута colour сущности Car:
<dsContext>
<datasource id="carDs" class="com.company.sample.entity.Car" view="_local"/>
<collectionDatasource id="coloursDs" class="com.company.sample.entity.Colour" view="_local">
<query>select c from sample$Colour c</query>
</collectionDatasource>
</dsContext>
<layout>
<lookupPickerField datasource="carDs" property="colour" optionsDatasource="coloursDs"/>
Атрибуты lookupPickerField
align - caption - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - filterMode - height - icon - id - inputPrompt - metaClass - newOptionAllowed - newOptionHandler - nullName - optionsDatasource - pageLength - property - required - requiredMessage - stylename - tabIndex - visible - width
Элементы lookupPickerField
Предопределённые стили lookupPickerField
API
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler - setOptionsStyleProvider
Текстовое поле, в которое данные вводятся в определенном формате. MaskedField удобно использовать, например, для ввода телефонных номеров.
XML-имя компонента: maskedField.
Компонент MaskedField реализован только для блока Web Client.
MaskedField в основном повторяет функциональность TextField, за исключением того, что ему нельзя установить datatype. То есть MaskedField предназначен для работы только с текстом и строковыми атрибутами сущностей. MaskedField имеет следующие специфические атрибуты:
Пример текстового поля с маской для ввода номеров телефонов:
<maskedField id="phoneNumberField" mask="(###)###-##-##" valueMode="masked"/>
<button caption="msg://showPhoneNumberBtn" invoke="showPhoneNumber"/>
@Inject
private MaskedField phoneNumberField;
public void showPhoneNumber(){
showNotification((String) phoneNumberField.getValue(), NotificationType.HUMANIZED);
}
Атрибуты maskedField
align - caption - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - mask - maxLength - property - required - requiredMessage - stylename - tabIndex - trim - valueMode - visible - width
Элементы maskedField
API
addEnterPressListener - addValueChangeListener - setContextHelpIconClickHandler
Компонент, который обеспечивает выбор из списка опций, используя группу переключателей для выбора единственного значения или группу флажков для выбора нескольких значений.
XML-имя компонента: optionsGroup.
Компонент OptionsGroup реализован для блоков Web Client и Desktop Client.
<dsContext>
<datasource id="roleDs" class="com.haulmont.cuba.security.entity.Role" view="_local"/>
</dsContext>
<layout>
<optionsGroup datasource="roleDs" property="type"/>
Как видно из примера, в экране описывается источник данных roleDs для сущности Role. В компоненте optionsGroup в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено.
В результате компонент примет следующий вид:
<optionsGroup id="numberOfSeatsField"/>
Затем инжектируем компонент в контроллер и в методе init() задаем ему список опций:
@Inject
protected OptionsGroup numberOfSeatsField;
@Override
public void init(Map<String, Object> params) {
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(4);
list.add(5);
list.add(7);
numberOfSeatsField.setOptionsList(list);
}
Компонент примет следующий вид:
При этом метод getValue() компонента в зависимости от выбранной опции будет возвращать Integer значения 2,4,5,7.
@Inject
protected OptionsGroup numberOfSeatsField;
@Override
public void init(Map<String, Object> params) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("two", 2);
map.put("four", 4);
map.put("five", 5);
map.put("seven", 7);
numberOfSeatsField.setOptionsMap(map);
}
Компонент примет следующий вид:
При этом метод getValue() компонента в зависимости от выбранной опции будет возвращать Integer значения 2,4,5,7, а не строки, отображаемые на экране.
<dsContext>
<collectionDatasource id="coloursDs" class="com.company.sample.entity.Colour" view="_local">
<query>select c from sample$Colour c</query>
</collectionDatasource>
</dsContext>
<layout>
<optionsGroup id="coloursField" optionsDatasource="coloursDs"/>
В данном случае компонент coloursField отобразит имена экземпляров сущности Colour, находящихся в источнике данных coloursDs, а его метод getValue() вернет выбранный экземпляр сущности.
С помощью атрибута captionProperty можно указать, какой атрибут сущности использовать вместо имени экземпляра для строковых названий опций.
Например, создадим в XML-дескрипторе экрана компонент:
<optionsGroup id="roleTypesField" multiselect="true"/>
И в контроллере зададим для него список опций - значения перечисления RoleType:
@Inject
protected OptionsGroup roleTypesField;
@Override
public void init(Map<String, Object> params) {
roleTypesField.setOptionsList(Arrays.asList(RoleType.values()));
}
Компонент примет следующий вид:
В данном случае метод getValue() компонента вернет объект типа java.util.List, содержащий значения RoleType.READONLY и RoleType.DENYING.
Этот пример иллюстрирует также способность компонента OptionsGroup автоматически отображать локализованные значения перечислений, входящих в модель данных приложения.
Чтобы программно задать выбор некоторых значений OptionsGroup, нужно передать список значений в формате java.util.List в метод setValue():
optionsGroup.setValue(Arrays.asList(RoleType.STANDARD, RoleType.ADMIN));
Атрибуты optionsGroup
align - caption - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - icon - id - multiselect - height - optionsDatasource - optionsEnum - orientation - property - required - requiredMessage - stylename - tabIndex - visible - width
Элементы optionsGroup
API
addValueChangeListener - setContextHelpIconClickHandler - setOptionsEnum - setOptionsList - setOptionsMap
OptionsList представляет собой вариацию компонента OptionsGroup с представлением опций в виде вертикального прокручиваемого списка. Если включена возможность множественного выбора, элементы могут быть выбраны с удерживанием клавиши Ctrl при клике или диапазона при удерживании клавиши Shift.
XML-имя компонента: optionsList.
Компонент OptionsList реализован для блока Web Client.
По умолчанию компонент OptionsList отображает первый пустой элемент в списке опций. Пустой элемент можно скрыть с помощью атрибута nullOptionVisible, установив ему значение false.
Единственная разница в API между OptionsList и OptionsGroup заключается в том, что OptionsList не имеет атрибута orientation.
Атрибуты optionsList
align - caption - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - multiselect - nullOptionVisible - optionsDatasource - optionsEnum - property - required - requiredMessage - stylename - tabIndex - visible - width
Элементы optionsList
API
addValueChangeListener - setContextHelpIconClickHandler
Текстовое поле, которое вместо символов, введенных пользователем, отображает эхо-символы.
XML-имя компонента: passwordField.
PasswordField реализован для блоков Web Client и Desktop Client.
PasswordField в основном аналогичен компоненту TextField, за исключением того, что ему нельзя установить datatype. То есть PasswordField предназначен для работы только с текстом и строковыми атрибутами сущностей.
Пример использования:
<passwordField id="passwordField" caption="msg://name"/>
<button caption="msg://buttonsName" invoke="showPassword"/>
@Inject
private PasswordField passwordField;
public void showPassword(){
showNotification((String) passwordField.getValue(), NotificationType.HUMANIZED);
}
Атрибут autocomplete позволяет включить сохранение паролей в веб браузере. По умолчанию отключено.
Атрибут capsLockIndicator принимает id компонента CapsLockIndicator, который отображает состояние клавиши Caps Lock при вводе пароля. Состояние Caps Lock отслеживается только тогда, когда поле passwordField находится в фокусе. Когда поле теряет фокус, статус Caps Lock автоматически становится "off".
Пример:
<passwordField id="passwordField"
capsLockIndicator="capsLockIndicator"/>
<capsLockIndicator id="capsLockIndicator"
align="MIDDLE_CENTER"
capsLockOffMessage="Caps Lock is OFF"
capsLockOnMessage="Caps Lock is ON"/>
Атрибуты passwordField
align - autocomplete - capsLockIndicator - caption - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - maxLength - property - required - requiredMessage - stylename - tabIndex - visible - width
Элементы passwordField
API
addValueChangeListener - setContextHelpIconClickHandler
PickerField позволяет отображать экземпляр сущности в текстовом поле и выполнять действия нажатием на кнопки справа.
XML-имя компонента: pickerField.
Компонент PickerField реализован для блоков Web Client и Desktop Client.
<dsContext>
<datasource id="carDs" class="com.company.sample.entity.Car" view="_local"/>
</dsContext>
<layout>
<pickerField datasource="carDs" property="colour"/>
Как видно из примера, в экране описывается источник данных carDs для некоторой сущности Car, имеющей атрибут colour. В элементе pickerField в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в компоненте. Атрибут сущности должен являться ссылкой на другую сущность, в приведенном примере это Colour.
Это можно сделать как в XML-дескрипторе с помощью вложенного элемента actions, так и программно в контроллере методом addAction().
<pickerField datasource="carDs" property="colour">
<actions>
<action id="lookup"/>
<action id="open"/>
<action id="clear"/>
</actions>
</pickerField>
Элемент action не дополняет, а переопределяет набор стандартных действий, поэтому необходимо указывать идентификаторы всех требуемых действий. Компонент примет следующий вид:
Для программного задания стандартных действий служат методы addLookupAction(), addOpenAction() и addClearAction(). Если компонент определен в XML-дескрипторе без вложенного элемента actions, то достаточно добавить недостающие действия:
@Inject
protected PickerField colourField;
@Override
public void init(Map<String, Object> params) {
colourField.addOpenAction();
}
Если же компонент создается в контроллере, то никаких действий по умолчанию он не получает, и необходимо добавить все нужные действия явно:
@Inject
protected ComponentsFactory componentsFactory;
@Override
public void init(Map<String, Object> params) {
PickerField colourField = componentsFactory.createComponent(PickerField.NAME);
colourField.setDatasource(carDs, "colour");
colourField.addLookupAction();
colourField.addOpenAction();
colourField.addClearAction();
}
Стандартные действия можно параметризовать. В XML-дескрипторе возможности для этого ограничены: существует только атрибут openType, в котором можно задать режим открытия экрана выбора (для LookupAction) или редактирования (для OpenAction).
При программном создании действий можно задать любые свойства объектов PickerField.LookupAction, PickerField.OpenAction и PickerField.ClearAction, возвращаемых методами добавления стандартных действий. Например, так можно задать специфический экран выбора:
PickerField.LookupAction lookupAction = customerField.addLookupAction();
lookupAction.setLookupScreen("customerLookupScreen");
Подробнее см. JavaDocs классов стандартных действий.
<pickerField datasource="carDs" property="colour">
<actions>
<action id="lookup"/>
<action id="show" icon="PICKERFIELD_OPEN"
invoke="showColour" caption=""/>
</actions>
</pickerField>
Программно задать произвольное действие можно следующим образом:
protected PickerField colourField;
@Override
public void init(Map<String, Object> params) {
colourField.addAction(new AbstractAction("show") {
@Override
public void actionPerform(Component component) {
showColour(colourField.getValue());
}
@Override
public String getCaption() {
return "";
}
@Override
public String getIcon() {
return "icons/show.png";
}
});
}
Декларативное и программное создание действий подробно описано в разделе Действия. Интерфейс Action.
<pickerField id="colourField" metaClass="sample$Colour"/>
Экземпляр выбранной сущности можно получить, инжектировав компонент в контроллер и вызвав его метод getValue().
|
Для правильной работы компонента PickerField необходима либо установка атрибута metaClass, либо одновременная установка атрибутов datasource и property. |
Атрибуты pickerField
align - caption - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - metaClass - property - required - requiredMessage - stylename - tabIndex - visible - width
Элементы pickerField
API
addAction - addClearAction - addLookupAction - addOpenAction - addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler - setLookupScreen
Кнопка с выпадающим меню. Меню может содержать список действий или отображать собственное содержимое.
XML-имя компонента: popupButton.
Компонент реализован для блоков Web Client и Desktop Client.
Кнопка PopupButton может содержать текст, заданный с помощью атрибута caption, или значок (или и то, и другое). Всплывающую подсказку можно задать с помощью атрибута description. На рисунке ниже отражены разные виды кнопок:
Элементы popupButton:
Отображаются только следующие свойства действий: caption, enable, visible. Свойства description, shortcut игнорируются. Обработка свойства icon зависит от свойства приложения cuba.gui.showIconsForPopupMenuActions и от атрибута showActionIcons компонента. Последний имеет приоритет.
Пример кнопки с выпадающим списком, содержащим два действия:
<popupButton id="popupButton" caption="msg://popupButton" description="Press me">
<actions>
<action id="popupAction1" caption="msg://action1" invoke="someAction1"/>
<action id="popupAction2" caption="msg://action2" invoke="someAction2"/>
</actions>
</popupButton>
Действия для popupButton можно как создать с нуля, так и использовать действия, уже созданные для какого-либо элемента в данном экране, например:
<popupButton id="popupButton">
<actions>
<action id="ordersTable.create"/>
<action id="ordersTable.edit"/>
<action id="ordersTable.remove"/>
</actions>
</popupButton>
Пример кнопки с собственным содержимым:
<popupButton id="popupButton"
caption="Settings"
align="MIDDLE_CENTER"
icon="font-icon:GEARS"
closePopupOnOutsideClick="true"
popupOpenDirection="BOTTOM_CENTER">
<popup>
<vbox width="250px"
height="AUTO"
spacing="true"
margin="true">
<label value="Settings"
align="MIDDLE_CENTER"
stylename="h2"/>
<progressBar caption="Progress"
width="100%"/>
<textField caption="New title"
width="100%"/>
<lookupField caption="Status"
optionsEnum="com.haulmont.cuba.core.global.SendingStatus"
width="100%"/>
<hbox spacing="true">
<button caption="Save" icon="SAVE"/>
<button caption="Reset" icon="REMOVE"/>
</hbox>
</vbox>
</popup>
</popupButton>
Атрибуты popupButton:
Методы интерфейса PopupButton:
popupButton.addPopupVisibilityListener(popupVisibilityEvent -> {
showNotification("Popup visibility changed");
});
Атрибуты popupButton
align - autoClose - caption - closePopupOnOutsideClick - description - enable - icon - id - menuWidth - popupOpenDirection - showActionIcons - stylename - tabIndex - togglePopupVisibilityOnClick - visible - width
Элементы popupButton
API
PopupView представляет собой компонент, позволяющий открывать popup с контейнером. Контейнер может открываться по клику на минимизированное значение или программно. Он может быть закрыт отведением курсора или по клику вне контейнера.
Обычный PopupView со скрытым и видимым popup-ом:
Рисунок 20. Popup скрыт
Рисунок 21. Popup открыт
Компонент реализован для блока Web Client.
Пример использования PopupView, где минимизированное значение получено из пакета локализации:
<popupView id="popupView"
minimizedValue="msg://minimizedValue"
caption="PopupView caption">
<vbox width="60px" height="40px">
<label value="Content" align="MIDDLE_CENTER"/>
</vbox>
</popupView>
Содержимое popup-а должно быть контейнером, например BoxLayout.
Методы PopupView:
@Inject
private PopupView popupView;
@Override
public void init(Map<String, Object> params) {
popupView.setPopupVisible(true);
}
@Inject
private PopupView popupView;
@Override
public void init(Map<String, Object> params) {
popupView.setMinimizedValue("Hello world!");
}
@Inject
private PopupView popupView;
@Override
public void init(Map<String, Object> params) {
popupView.addPopupVisibilityListener(event ->
showNotification(event.isPopupVisible() ? "The popup is visible" : "The popup is hidden",
NotificationType.HUMANIZED));
}
Атрибуты PopupView:
Атрибуты popupView
caption - captionAsHtml - description - height - hideOnMouseOut - icon - id - minimizedValue - stylename - visible - width
API
Компонент ProgressBar служит для отображения хода выполнения некоторого длительного процесса.
XML-имя компонента: progressBar
Компонент реализован для блоков Web Client и Desktop Client.
Пример использования компонента совместно с механизмом фоновых задач:
<progressBar id="progressBar" width="100%"/>
@Inject
protected ProgressBar progressBar;
@Inject
protected BackgroundWorker backgroundWorker;
private static final int ITERATIONS = 5;
@Override
public void init(Map<String, Object> params) {
BackgroundTask<Integer, Void> task = new BackgroundTask<Integer, Void>(300, this) {
@Override
public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
for (int i = 1; i <= ITERATIONS; i++) {
TimeUnit.SECONDS.sleep(2); // time consuming task
taskLifeCycle.publish(i);
}
return null;
}
@Override
public void progress(List<Integer> changes) {
float lastValue = changes.get(changes.size() - 1);
progressBar.setValue(lastValue / ITERATIONS);
}
};
BackgroundTaskHandler taskHandler = backgroundWorker.handle(task);
taskHandler.execute();
}
Здесь в методе BackgroundTask.progress(), выполняемом в UI-потоке, компоненту ProgressBar устанавливается текущее значение. Значением компонента должно быть число типа float от 0.0 до 1.0.
Изменения значения компонента ProgressBar можно отслеживать с помощью слушателя ValueChangeListener.
Если выполняемый процесс не может передавать информацию о прогрессе, то с помощью атрибута indeterminate можно задать отображение неопределенного состояния индикатора. Если значение атрибута равно true, то индикатор отображает неопределенное состояние. По умолчанию false. Например:
<progressBar id="progressBar" width="100%" indeterminate="true"/>
По умолчанию неопределённый индикатор представляет собой горизонтальную полосу. Чтобы отобразить ProgressBar в виде крутящегося колесика, установите атрибут stylename="indeterminate-circle".
Чтобы изменить форму индикатора на точку, перемещающуюся по полосе, вместо растущей полосы, используйте стиль point:
progressBar.setStyleName(HaloTheme.PROGRESSBAR_POINT);
Атрибуты progressBar
align - enable - height - id - indeterminate - stylename - visible - width
Предопределенные стили progressBar
API
RelatedEntities - компонент в виде кнопки с выпадающим списком, при нажатии показывающим список классов, связанных с сущностью, экземпляры которой отображаются в таблице. Пользователь выбирает интересующий его класс сущности, после чего открывается новый браузер с экземплярами данной сущности, связанными с выбранными экземплярами в начальной таблице.
XML-имя компонента: relatedEntities
Компонент реализован для блоков Web Client и Desktop Client.
При отборе связанных сущностей для отображения учитываются разрешения пользователя на открытие экранов, чтение сущностей и чтение атрибутов.
По умолчанию для выбранного в списке класса сущности открывается браузер сущности, определенный по соглашениям ({entity_name}.browse, {entity_name}.lookup). Опционально, экран можно явно задать в компоненте.
В открытом браузере динамически создается фильтр, который выбирает связанные с выбранными сущностями записи.
Пример описания компонента в XML-дескрипторе экрана:
<table id="invoiceTable"
multiselect="true"
width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
</actions>
<buttonsPanel id="buttonsPanel">
<button id="createBtn"
action="invoiceTable.create"/>
<button id="editBtn"
action="invoiceTable.edit"/>
<button id="removeBtn"
action="invoiceTable.remove"/>
<relatedEntities for="invoiceTable"
openType="NEW_TAB">
<property name="invoiceItems"
screen="sales$InvoiceItem.lookup"
filterCaption="msg://invoiceItems"/>
</relatedEntities>
</buttonsPanel>
Атрибут for является обязательным. В нем указывается идентификатор таблицы.
Атрибут openType="NEW_TAB" устанавливает режим открытия браузера (новая вкладка). По умолчанию браузер открывается в текущей вкладке.
Элемент property позволяет явно задать связанную сущность, которая будет отображаться в выпадающем списке.
Атрибуты property:
Атрибут exclude позволяет исключить определенные связанные сущности из числа отображаемых. В качестве значения указывается регулярное выражение, отбирающее ссылочные атрибуты текущей сущности для исключения.
В платформе есть API для открытия экранов связанных сущностей без использования компонента RelatedEntities: интерфейс RelatedEntitiesAPI и его реализация RelatedEntitiesBean. Логика задаётся методом openRelatedScreen(), который принимает коллекцию сущностей с одной стороны отношения, MetaClass отдельной сущности из этой коллекции и поле, являющееся ссылкой на связанные сущности.
<button id="related"
caption="Related customer"
invoke="onRelatedClick"/>
import com.company.sales.entity.Order;
import com.haulmont.cuba.gui.components.AbstractLookup;
import com.haulmont.cuba.gui.components.Table;
import com.haulmont.cuba.gui.relatedentities.RelatedEntitiesAPI;
import javax.inject.Inject;
public class OrderBrowse extends AbstractLookup {
@Inject
private RelatedEntitiesAPI relatedEntitiesAPI;
@Inject
private Table<Order> ordersTable;
public void onRelatedClick() {
relatedEntitiesAPI.openRelatedScreen(ordersTable.getSelected(),
Order.class, "customer");
}
}
По умолчанию метод открывает стандартный экран просмотра списка. Дополнительно можно указать параметр RelatedScreenDescriptor, если требуется открыть экран, отличный от стандартного, или открыть его с параметрами. RelatedScreenDescriptor - это простой Java-объект, хранящий идентификатор экрана (String), тип его открытия (WindowManager.OpenType), заголовок фильтра (String) и параметры экрана (Map<String, Object>).
relatedEntitiesAPI.openRelatedScreen(ordersTable.getSelected(),
Order.class, "customer",
new RelatedScreenDescriptor("sales$Customer.lookup", OpenType.DIALOG));
Атрибуты relatedEntities
align - caption - description - enable - exclude - for - icon - id - openType - stylename - tabIndex - visible - width
Атрибуты property
caption - filterCaption - name - screen
Текстовая область для отображения и ввода форматированного текста.
XML-имя компонента: richTextArea
Компонент RichTextArea реализован только для блока Web Client.
RichTextArea в основном повторяет функциональность TextField, за исключением того, что ему нельзя установить datatype. То есть RichTextArea предназначен для работы только с текстом и строковыми атрибутами сущностей.
Атрибуты richTextArea
align - caption - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - property - required - requiredMessage - stylename - tabIndex - visible - width
API
addValueChangeListener - setContextHelpIconClickHandler
Компонент SearchPickerField служит для поиска экземпляров сущностей по вводимой пользователем строке. Пользователю достаточно ввести несколько символов и нажать клавишу Enter. Если поиск дал несколько совпадений, найденные значения отображаются в виде выпадающего списка. Если же критерию поиска соответствует только один экземпляр, он сразу становится значением компонента. SearchPickerField позволяет также выполнять действия нажатием на кнопки справа.
См. также SuggestionPickerField.
XML-имя компонента: searchPickerField.
Компонент реализован для блоков Web Client и Desktop Client.
<dsContext>
<datasource id="carDs" class="com.company.sample.entity.Car" view="_local"/>
<collectionDatasource id="coloursDs" class="com.company.sample.entity.Colour" view="_local">
<query>
select c from sample$Colour c
where c.name like :(?i)custom$searchString
</query>
</collectionDatasource>
</dsContext>
<layout>
<searchPickerField datasource="carDs" property="colour" optionsDatasource="coloursDs"/>
В данном случае компонент будет искать экземпляры сущности Colour по вхождению подстроки в ее атрибут name. Префикс (?i) служит для регистро-независимого поиска (см. Поиск подстроки без учета регистра). Выбранное значение подставится в атрибут colour сущности Car, находящейся в источнике данных carDs.
Атрибут escapeValueForLike со значением true позволяет искать значения, содержащие специальные символы %, \ и _ при помощи like. Чтобы использовать escapeValueForLike = true, необходимо добавить в запрос источника данных escape-значение:
select c from ref$Colour c
where c.name like :(?i)custom$searchString or c.description like :(?i)custom$searchString escape '\'
Атрибут escapeValueForLike работает со всеми типами базы данных, кроме HSQLDB.
Пример реализации методов для вывода на экран сообщений:
@Inject
private SearchPickerField colourField;
@Override
public void init(Map<String, Object> params) {
colourField.setSearchNotifications(new SearchField.SearchNotifications() {
@Override
public void notFoundSuggestions(String filterString) {
showNotification("No colours found for search string: " + filterString,
NotificationType.TRAY);
}
@Override
public void needMinSearchStringLength(String filterString, int minSearchStringLength) {
showNotification("Minimum length of search string is " + minSearchStringLength,
NotificationType.TRAY);
}
});
}
и open
. Атрибуты searchPickerField
align - caption - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - filterMode - height - icon - id - inputPrompt - metaClass - minSearchStringLength - newOptionAllowed - newOptionHandler - nullName - optionsDatasource - property - required - requiredMessage - stylename - tabIndex - visible - width
Элементы searchPickerField
Предопределённые стили searchPickerField
API
addValueChangeListener - setContextHelpIconClickHandler
Компонент SideMenu позволяет создать боковое главное меню в главном окне приложения, управлять элементами меню, добавлять значки и ярлыки и применять стили.
Его также можно использовать в экранах приложения как обычный визуальный компонент. Для этого необходимо добавить пространство имён xmlns:main="http://schemas.haulmont.com/cuba/mainwindow.xsd" в дескриптор экрана.
XML-имя компонента: sideMenu.
Пример описания компонента в XML-дескрипторе экрана:
<main:sideMenu id="sideMenu"
width="100%"
selectOnClick="true"/>
CUBA Studio предоставляет готовый шаблон главного экрана с реализацией компонента sideMenu и готовыми стилями боковой панели:
<layout>
<hbox id="horizontalWrap"
expand="workArea"
height="100%"
stylename="c-sidemenu-layout"
width="100%">
<vbox id="sideMenuPanel"
expand="sideMenu"
height="100%"
margin="false,false,true,false"
spacing="true"
stylename="c-sidemenu-panel"
width="250px">
<hbox id="appTitleBox"
spacing="true"
stylename="c-sidemenu-title"
width="100%">
<label id="appTitleLabel"
align="MIDDLE_CENTER"
value="mainMsg://application.logoLabel"/>
</hbox>
<embedded id="logoImage"
align="MIDDLE_CENTER"
stylename="c-app-icon"
type="IMAGE"/>
<hbox id="userInfoBox"
align="MIDDLE_CENTER"
expand="userIndicator"
margin="true"
spacing="true"
width="100%">
<main:userIndicator id="userIndicator"
align="MIDDLE_CENTER"/>
<main:newWindowButton id="newWindowButton"
description="mainMsg://newWindowBtnDescription"
icon="app/images/new-window.png"/>
<main:logoutButton id="logoutButton"
description="mainMsg://logoutBtnDescription"
icon="app/images/exit.png"/>
</hbox>
<main:sideMenu id="sideMenu"
width="100%"/>
<main:ftsField id="ftsField"
width="100%"/>
</vbox>
<main:workArea id="workArea"
height="100%">
<main:initialLayout margin="true"
spacing="true">
<label id="welcomeLabel"
align="MIDDLE_CENTER"
stylename="c-welcome-text"
value="mainMsg://application.welcomeText"/>
</main:initialLayout>
</main:workArea>
</hbox>
</layout>
Атрибуты sideMenu:
Методы интерфейса SideMenu:
Компонент SideMenu предназначен для отображения элементов меню. Чтобы создать элемент меню, используется API компонента MenuItem в контроллере экрана. Методы, перечисленные ниже, можно использовать для динамического обновления элементов меню, реализуя бизнес-логику приложения. Пример программного создания элемента меню:
SideMenu.MenuItem item = sideMenu.createMenuItem("special");
item.setCaption("Daily offer");
item.setBadgeText("New");
item.setIconFromSet(CubaIcon.GIFT);
sideMenu.addMenuItem(item,0);
Методы интерфейса MenuItem:
int count = 5;
SideMenu.MenuItem item = sideMenu.createMenuItem("count");
item.setCaption("Messages");
item.setBadgeText(count + " new");
item.setIconFromSet(CubaIcon.ENVELOPE);
sideMenu.addMenuItem(item,0);
Текст ярлыка можно обновлять автоматически с помощью компонента Timer:
public void updateCounters(Timer source) {
sideMenu.getMenuItemNN("sales")
.setBadgeText(String.valueOf(LocalTime.MIDNIGHT.minusSeconds(timerCounter-source.getDelay())));
timerCounter++;
}
Стандартный шаблон главного экрана с sideMenu стилизован несколькими предопределёнными стилями: c-sidemenu-layout, c-sidemenu-panel и c-sidemenu-title. Стиль бокового меню по умолчанию c-sidemenu поддерживается только в рамках темы Halo и темах, её расширяющих. В теме Havana стили sideMenu не поддерживаются.
Атрибуты sideMenu
align - caption - description - enable - height - icon - id - selectOnClick - stylename - tabIndex - visible - width
Атрибуты ftsfield
align - caption - description - enable - height - icon - id - stylename - visible - width
API sideMenu
addMenuItem - createMenuItem - removeMenuItem
API menuItem
addChildItem - removeChildItem - setBadgeText - setCaption - setCaptionAsHtml - setCommand - setExpanded - setIcon - setStyleName - setTestId
SourceCodeEditor - компонент для отображения и ввода исходного кода. Он представляет собой многострочное текстовое поле с возможностью подсветки кода и отображения полей печати и номеров строк.
XML-имя компонента: sourceCodeEditor.
Компонент SourceCodeEditor реализован для блока Web Client.
SourceCodeEditor в основном повторяет функциональность TextField и имеет следующие специфические атрибуты:
Следующие свойства можно легко изменять в работающем приложении:
Ниже приведён пример компонента SourceCodeEditor с динамически настраиваемыми атрибутами.
XML-дескриптор:
<hbox spacing="true">
<checkBox id="highlightActiveLineCheck" align="BOTTOM_LEFT" caption="Highlight Active Line"/>
<checkBox id="printMarginCheck" align="BOTTOM_LEFT" caption="Print Margin"/>
<checkBox id="showGutterCheck" align="BOTTOM_LEFT" caption="Show Gutter"/>
<lookupField id="modeField" align="BOTTOM_LEFT" caption="Mode" required="true"/>
</hbox>
<sourceCodeEditor id="simpleCodeEditor" width="100%"/>
Контроллер:
@Inject
private LookupField modeField;
@Inject
private SourceCodeEditor simpleCodeEditor;
@Inject
private CheckBox highlightActiveLineCheck;
@Inject
private CheckBox printMarginCheck;
@Inject
private CheckBox showGutterCheck;
@Override
public void init(Map<String, Object> params) {
highlightActiveLineCheck.setValue(simpleCodeEditor.isHighlightActiveLine());
highlightActiveLineCheck.addValueChangeListener(e -> simpleCodeEditor.setHighlightActiveLine(Boolean.TRUE.equals(e.getValue())));
printMarginCheck.setValue(simpleCodeEditor.isShowPrintMargin());
printMarginCheck.addValueChangeListener(e -> simpleCodeEditor.setShowPrintMargin(Boolean.TRUE.equals(e.getValue())));
showGutterCheck.setValue(simpleCodeEditor.isShowGutter());
showGutterCheck.addValueChangeListener(e -> simpleCodeEditor.setShowGutter(Boolean.TRUE.equals(e.getValue())));
Map<String, Object> modes = new HashMap<>();
for (SourceCodeEditor.Mode mode : SourceCodeEditor.Mode.values()) {
modes.put(mode.toString(), mode);
}
modeField.setOptionsMap(modes);
modeField.setValue(SourceCodeEditor.Mode.Text);
modeField.addValueChangeListener(e -> simpleCodeEditor.setMode((SourceCodeEditor.Mode) e.getValue()));
}
Результат выполения кода:
Компонент SourceCodeEditor также поддерживает автодополнение кода, определяемое с помощью класса Suggester. Чтобы подключить автодополнение, необходимо переопределить и вызвать метод setSuggester, например:
@Inject
private SourceCodeEditor suggesterCodeEditor;
@Inject
private CollectionDatasource<User, UUID> usersDs;
@Override
public void init(Map<String, Object> params) {
suggesterCodeEditor.setSuggester((source, text, cursorPosition) -> {
List<Suggestion> suggestions = new ArrayList<>();
usersDs.refresh();
for (User user : usersDs.getItems()) {
suggestions.add(new Suggestion(source, user.getLogin(), user.getName(), null, -1, -1));
}
return suggestions;
});
}
Результат:
Атрибуты sourceCodeEditor
align - caption - colspan - datasource - description - editable - enable - handleTabKey - height - highlightActiveLine - icon - id - mode - printMargin - property - required - requiredMessage - rowspan - showGutter - stylename - tabIndex - visible - width
API
addValueChangeListener - setContextHelpIconClickHandler
Компонент SuggestionField предназначен для поиска экземпляров сущности по строке, вводимой пользователем. Он отличается от SuggestionPickerField тем, что может возвращать любые типы значений, например, сущности, строки или перечисления, а также не имеет кнопок для действий. Список опций загружается асинхронно в соответствии с логикой, задаваемой разработчиком на стороне сервера.
XML-имя компонента: suggestionField.
Компонент реализован для блока Web Client.
Атрибуты suggestionField:
Возможные значения:
Элементы suggestionField:
<suggestionField id="suggestionField"
captionProperty="login">
<query entityClass="com.haulmont.cuba.security.entity.User"
escapeValueForLike="true"
view="user.edit"
searchStringFormat="%$searchString%">
select e from sec$User e where e.login like :searchString escape '\'
</query>
</suggestionField>
Если элемент query не задан, то список опций должен быть предоставлен объектом типа SearchExecutor, созданным программно (см. ниже).
Как правило, для компонента достаточно установить SearchExecutor. SearchExecutor - это функциональный интерфейс, содержащий один метод: List<E> search(String searchString, Map<String, Object> searchParams):
suggestionField.setSearchExecutor((searchString, searchParams) -> {
return Arrays.asList(entity1, entity2, ...);
});
SearchExecutor может возвращать любые типы значений, например, сущности, строки или перечисления.
customersDs.refresh();
List<Customer> customers = new ArrayList<>(customersDs.getItems());
suggestionField.setSearchExecutor((searchString, searchParams) ->
customers.stream()
.filter(customer -> StringUtils.containsIgnoreCase(customer.getName(), searchString))
.collect(Collectors.toList()));
List<String> strings = Arrays.asList("Red", "Green", "Blue", "Cyan", "Magenta", "Yellow");
stringSuggestionField.setSearchExecutor((searchString, searchParams) ->
strings.stream()
.filter(str -> StringUtils.containsIgnoreCase(str, searchString))
.collect(Collectors.toList()));
List<SendingStatus> enums = Arrays.asList(SendingStatus.values());
enumSuggestionField.setSearchExecutor((searchString, searchParams) ->
enums.stream()
.map(sendingStatus -> messages.getMessage(sendingStatus))
.filter(str -> StringUtils.containsIgnoreCase(str, searchString))
.collect(Collectors.toList()));
List<OptionWrapper> wrappers = Arrays.asList(
new OptionWrapper("One", 1),
new OptionWrapper("Two", 2),
new OptionWrapper("Three", 3);
suggestionField.setSearchExecutor((searchString, searchParams) ->
wrappers.stream()
.filter(optionWrapper -> StringUtils.containsIgnoreCase(optionWrapper.getCaption(), searchString))
.collect(Collectors.toList()));
|
Метод search() выполняется в фоновом потоке, поэтому он не может обращаться к визуальным компонентам или источникам данных, связанным с визуальными компонентами. Можно использовать DataManager или напрямую вызывать сервисы среднего слоя, или обрабатывать и возвращать данные, предварительно загруженные в экран. |
Параметр searchString может быть использован для фильтрации кандидатов по строке, введенной пользователем. Чтобы искать по значениям, содержащим специальные символы, используйте метод escapeForLike():
suggestionField.setSearchExecutor((searchString, searchParams) -> {
searchString = QueryUtils.escapeForLike(searchString);
return dataManager.loadList(LoadContext.create(Customer.class).setQuery(
LoadContext.createQuery("select c from sample$Customer c where c.name like :name order by c.name escape '\\'")
.setParameter("name", "%" + searchString + "%")));
});
suggestionField.setOptionsStyleProvider((field, item) -> {
User user = (User) item;
switch (user.getGroup().getName()) {
case "Company":
return "company";
case "Premium":
return "premium";
default:
return "company";
}
});
Атрибуты suggestionField
align - asyncSearchDelayMs - caption - captionProperty - colspan - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - inputPrompt - minSearchStringLength - popupWidth - property - required - requiredMessage - responsive - rowspan - stylename - suggestionsLimit - tabIndex - visible - width
Элементы suggestionField
Атрибуты query
entityClass - escapeValueForLike - searchStringFormat - view
Предопределенные стили suggestionField
API
addValueChangeListener - setContextHelpIconClickHandler
5.5.2.1.39. SuggestionPickerField
Компонент SuggestionPickerField предназначен для поиска экземпляров сущности по строке, вводимой пользователем. Он отличается от SearchPickerField тем, что обновляет список опций при каждом вводе символа пользователем без необходимости нажимать Enter. Список опций загружается асинхронно в соответствии с логикой, задаваемой разработчиком на стороне сервера.
SuggestionPickerField является также PickerField и может содержать действия, отображаемые кнопками справа.
XML-имя компонента: suggestionPickerField.
Компонент реализован для блока Web Client.
SuggestionPickerField используется для выбора значений ссылочных атрибутов, поэтому для компонента обычно указываются атрибуты datasource и property:
<dsContext>
<datasource id="orderDs"
class="com.company.sample.entity.Order"
view="order-view"/>
</dsContext>
<layout>
<suggestionPickerField id="suggestionPickerField"
captionProperty="name"
datasource="orderDs"
property="customer"/>
</layout>
Атрибуты suggestionPickerField:
Возможные значения:
|
Внешний вид поля suggestionPickerField и его всплывающей подсказки со списком опций можно настроить с помощью атрибута stylename. Значение stylename для подсказки должно иметь то же имя, что и стиль основного компонента: к примеру, если компонент имеет стиль "my-awesome-stylename", для его подсказки можно создать стиль "c-suggestionfield-popup my-awesome-stylename". |
Элементы suggestionPickerField:
Простой пример использования SuggestionPickerField
Как правило, для компонента достаточно установить SearchExecutor. SearchExecutor - это функциональный интерфейс, содержащий один метод: List<E extends Entity> search(String searchString, Map<String, Object> searchParams):
suggestionPickerField.setSearchExecutor((searchString, searchParams) -> {
return Arrays.asList(entity1, entity2, ...);
});
|
Метод search() выполняется в фоновом потоке, поэтому он не может обращаться к визуальным компонентам или источникам данных, связанным с визуальными компонентами. Можно использовать DataManager или напрямую вызывать сервисы среднего слоя, или обрабатывать и возвращать данные, предварительно загруженные в экран. |
Параметр searchString может быть использован для фильтрации кандидатов по строке, введенной пользователем. Чтобы искать по значениям, содержащим специальные символы, используйте метод escapeForLike():
suggestionPickerField.setSearchExecutor((searchString, searchParams) -> {
searchString = QueryUtils.escapeForLike(searchString);
return dataManager.loadList(LoadContext.create(Customer.class).setQuery(
LoadContext.createQuery("select c from sample$Customer c where c.name like :name order by c.name escape '\\'")
.setParameter("name", "%" + searchString + "%")));
});
Использование ParametrizedSearchExecutor
В примерах выше параметр searchParams является пустым. Для поиска с параметрами используется ParametrizedSearchExecutor:
suggestionPickerField.setSearchExecutor(new SuggestionField.ParametrizedSearchExecutor<Customer>(){
@Override
public Map<String, Object> getParams() {
return ParamsMap.of(...);
}
@Override
public List<Customer> search(String searchString, Map<String, Object> searchParams) {
return executeSearch(searchString, searchParams);
}
});
Использование EnterActionHandler и ArrowDownActionHandler
Компонент также может быть использован с обработчиками событий EnterActionHandler и ArrowDownActionHandler. Эти листнеры срабатывают, когда пользователь нажимает клавиши Enter или Arrow Down при скрытом всплывающем окне для подсказок. Они также представляют собой функциональные интерфейсы с единственным методом с одним параметром - currentSearchString. Вы можете настроить и свои обработчики событий и использовать метод showSuggestions() интерфейса SuggestionField, который принимает список сущностей, для отображения подсказок:
suggestionPickerField.setArrowDownActionHandler(currentSearchString -> {
List<Customer> suggestions = findSuggestions();
suggestionPickerField.showSuggestions(suggestions);
});
suggestionPickerField.setEnterActionHandler(currentSearchString -> {
List<Customer> suggestions = getDefaultSuggestions();
suggestionPickerField.showSuggestions(suggestions);
});
Атрибуты suggestionPickerField
align - asyncSearchDelayMs - caption - captionProperty - colspan - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - inputPrompt - metaClass - minSearchStringLength - popupWidth - property - required - requiredMessage - responsive - rowspan - stylename - suggestionsLimit - tabIndex - visible - width
Элементы suggestionPickerField
Предопределенные стили suggestionPickerField
API
addValueChangeListener - setContextHelpIconClickHandler - setOptionsStyleProvider
Компонент Table позволяет выводить информацию в табличном виде, сортировать данные, управлять колонками и заголовками таблицы, вызывать действия для выбранных строк.
XML-имя компонента: table
Компонент реализован для блоков Web Client и Desktop Client.
Пример описания таблицы в XML-дескрипторе экрана:
<dsContext>
<collectionDatasource id="ordersDs"
class="com.sample.sales.entity.Order"
view="order-with-customer">
<query>
select o from sales$Order o order by o.date
</query>
</collectionDatasource>
</dsContext>
<layout>
<table id="ordersTable" width="300px">
<columns>
<column id="date"/>
<column id="customer.name"/>
<column id="amount"/>
</columns>
<rows datasource="ordersDs"/>
</table>
Здесь в элементе dsContext определен источник данных collectionDatasource, который выбирает сущности Order с помощью JPQL запроса select o from sales$Order o order by o.date. Для компонента table в элементе rows указывается используемый источник данных, а в элементе columns - какие атрибуты сущности, содержащейся в источнике данных, использовать в качестве колонок.
Элементы table:
Для строк можно настроить отображение заголовков - задать каждой строке свой значок в дополнительной колонке слева. Для этого в контроллере экрана необходимо реализовать интерфейс ListComponent.IconProvider и установить его таблице:
@Inject
private Table<Customer> table;
@Override
public void init(Map<String, Object> params) {
table.setIconProvider(new ListComponent.IconProvider<Customer>() {
@Nullable
@Override
public String getItemIcon(Customer entity) {
CustomerGrade grade = entity.getGrade();
switch (grade) {
case PREMIUM: return "icons/premium_grade.png";
case HIGH: return "icons/high_grade.png";
case MEDIUM: return "icons/medium_grade.png";
default: return null;
}
}
});
}
Каждая колонка описывается во вложенном элементе column со следующими атрибутами:
<columns>
<column id="date"/>
<column id="customer"/>
<column id="customer.name"/>
<column id="customer.address.country"/>
</columns>
в правой верхней части таблицы, если атрибут columnControlVisible таблицы не false. По умолчанию collapsed имеет значение false.
public void linkedMethod(Entity item, String columnId) {
Customer customer = (Customer) item;
showNotification(customer.getName());
}
<column id="priority.orderNo" captionProperty="priority.name" caption="msg://priority" />
В этом случае в колонке будет отображаться название приоритета, а сортировка колонки будет осуществляться по атрибуту orderNo.
<columns>
<column id="name"/>
<column id="imageFile"
generator="generateImageFileCell"/>
</columns>
public Component generateImageFileCell(Employee entity) {
Embedded embedded = componentsFactory.createComponent(Embedded.class);
embedded.setType(Embedded.Type.IMAGE);
FileDescriptor userImageFile = entity.getImageFile();
FileDataProvider dataProvider = new FileDataProvider(userImageFile);
embedded.setSource(userImageFile.getId() + "." + userImageFile.getExtension(), dataProvider);
return embedded;
}
Он может быть использован вместо передачи реализации Table.ColumnGenerator в метод addGeneratedColumn().
<column id="date">
<formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter"
format="yyyy-MM-dd HH:mm:ss"
useUserTimezone="true"/>
</column>
Компонент RowsCount может также отобразить общее число записей, возвращаемых текущим запросом в источнике данных, без извлечения этих записей. Для этого при щелчке пользователя на знаке "?" он вызывает метод AbstractCollectionDatasource.getCount(), что приводит к выполнению в БД запроса с такими же, как у текущего запроса условиями, но с агрегатной функцией COUNT(*) вместо результатов. Полученное число отображается вместо знака "?".
Атрибуты table:
/
. Сортировку некоторой отдельной колонки можно запретить с помощью атрибута sortable этой колонки. При включенной с помощью элемента rowsCount (см. выше) страничной загрузке таблицы сортировка производится разными способами в зависимости от того, умещаются ли все записи на одной странице. Если умещаются, то сортировка производится в памяти, без обращений к БД. Если же страниц больше одной, то сортировка производится на базе данных путем отправки нового запроса с соответствующим ORDER BY.
Колонка таблицы может ссылаться на локальный атрибут или на связанную сущность. Например:
<table id="ordersTable">
<columns>
<column id="customer.name"/> <!-- the 'name' attribute of the 'Customer' entity -->
<column id="contract"/> <!-- the 'Contract' entity -->
</columns>
<rows datasource="ordersDs"/>
</table>
В последнем случае, сортировка на базе данных производится по атрибутам, указанным в аннотации @NamePattern связанной сущности. Если у связанной сущности нет такой аннотации, то сортировка производится в памяти только в пределах текущей страницы.
Если колонка таблицы ссылается на неперсистентный атрибут, то сортировка на базе данных производится по атрибутам, указанным в параметре related() аннотации @MetaProperty. Если такой параметр не указан, то сортировка производится в памяти только в пределах текущей страницы.
Если таблица соединена со вложенным источником данных, который содержит коллекцию связанных сущностей, то для того, чтобы таблицу можно было сортировать, атрибут-коллекция должен быть упорядоченного типа (List или LinkedHashSet). Если атрибут имеет тип Set, то атрибут sortable не оказывает влияния и пользователи не смогут сортировать таблицу.
. Механизм представлений реализован только для блока Web Client.
в правой части шапки таблицы. Флажками в меню отмечаются отображаемые в данный момент колонки.
Для агрегируемых колонок необходимо указать элемент aggregation с атрибутом type, задающим функцию агрегации. По умолчанию в агрегируемых колонках поддерживаются только числовые типы данных, такие как Integer, Double, Long и BigDecimal. Агрегированные значения столбцов выводятся в дополнительной строке вверху таблицы. Пример описания таблицы с агрегацией:
<table id="itemsTable" aggregatable="true">
<columns>
<column id="product"/>
<column id="quantity"/>
<column id="amount">
<aggregation type="SUM"/>
</column>
</columns>
<rows datasource="itemsDs"/>
</table>
Элемент aggregation может также содержать атрибут strategyClass, указывающий класс, реализующий интерфейс AggregationStrategy interface (см. ниже пример установки стратегии агрегации программно).
Для отображения агрегированного значения в виде, отличном от стандартного для данного Datatype, для него можно указать Formatter:
<column id="amount">
<aggregation type="SUM">
<formatter class="com.company.sample.MyFormatter"/>
</aggregation>
</column>
Атрибут aggregationStyle позволяет задать положение строки агрегации: TOP или BOTTOM. По умолчанию используется TOP.
В дополнение к операциям, перечисленным выше, можно задать собственную стратегию агрегации путем создания класса, реализующего интерфейс AggregationStrategy, и передачи его методу setAggregation() класса Table.Column в составе экземпляра AggregationInfo. Например:
public class TimeEntryAggregation implements AggregationStrategy<List<TimeEntry>, String> {
@Override
public String aggregate(Collection<List<TimeEntry>> propertyValues) {
HoursAndMinutes total = new HoursAndMinutes();
for (List<TimeEntry> list : propertyValues) {
for (TimeEntry timeEntry : list) {
total.add(HoursAndMinutes.fromTimeEntry(timeEntry));
}
}
return StringFormatHelper.getTotalDayAggregationString(total);
}
@Override
public Class<String> getResultClass() {
return String.class;
}
}
AggregationInfo info = new AggregationInfo();
info.setPropertyPath(metaPropertyPath);
info.setStrategy(new TimeEntryAggregation());
Table.Column column = weeklyReportsTable.getColumn(columnId);
column.setAggregation(info);
Тип компонента для каждой редактируемой колонки выбирается автоматически на основании типа атрибута сущности. Например, для строковых и числовых атрибутов используется TextField, для Date - DateField, для перечислений - LookupField, для ссылок на другие сущности - PickerField.
Для редактируемой колонки типа Date можно дополнительно указать атрибуты dateFormat или resolution аналогично описанным для DateField.
Для редактируемой колонки, отображающей связанную сущность, можно дополнительно указать атрибуты optionsDatasource и captionProperty. При указании optionsDatasource вместо PickerField используется компонент LookupField.
Произвольно настроить отображение ячеек, в том числе для редактирования содержимого, можно с помощью метода Table.addGeneratedColumn() - см. ниже.
<table id="table"
stylename="no-stripes">
<columns>
<column id="product"/>
<column id="quantity"/>
</columns>
<rows datasource="itemsDs"/>
</table>
Чтобы применить стиль программно, выберите одну из констант класса HaloTheme с префиксом компонента TABLE_:
table.setStyleName(HaloTheme.TABLE_NO_STRIPES);
Стили компонента Table:
Методы интерфейса Table:
Метод generateCell() интерфейса Table.ColumnGenerator вызывается таблицей для каждой строки, и в него передается экземпляр сущности, отображаемой в данной строке. Метод generateCell() должен вернуть визуальный компонент, который и будет отображаться в ячейке.
Пример использования:
@Inject
protected Table carsTable;
@Inject
protected ComponentsFactory componentsFactory;
@Override
public void init(Map<String, Object> params) {
carsTable.addGeneratedColumn("colour", new Table.ColumnGenerator() {
@Override
public Component generateCell(Entity entity) {
LookupPickerField field = componentsFactory.createComponent(LookupPickerField.NAME);
field.setDatasource(carsTable.getItemDatasource(entity), "colour");
field.setOptionsDatasource(coloursDs);
field.addLookupAction();
field.addOpenAction();
return field;
}
});
}
В данном случае в ячейках колонки colour таблицы отображается компонент LookupPickerField. Компонент должен сохранять свое значение в атрибут colour сущности, экземпляр которой отображается в данной строке. Для этого у таблицы методом getItemDatasource() запрашивается источник данных для текущего экземпляра сущности, и передается компоненту LookupPickerField.
Если в ячейке необходимо отобразить просто динамически сформированный текст, вместо компонента Label используйте класс Table.PlainTextCell. Это упростит отрисовку и сделает таблицу быстрее.
Если в метод addGeneratedColumn() передан идентификатор колонки, не объявленной в XML-дескрипторе, то может понадобиться установить заголовок новой колонки следующим образом:
carsTable.getColumn("colour").setCaption("Colour");
Существует также более декларативный подход, использующий XML-атрибут generator.
table.requestFocus(item, "count");
Пример использования метода:
table.scrollTo(item);
Пример задания стилей:
@Inject
protected Table customersTable;
@Override
public void init(Map<String, Object> params) {
customersTable.setStyleProvider(new Table.StyleProvider() {
@Nullable
@Override
public String getStyleName(Entity entity, @Nullable String property) {
Customer customer = (Customer) entity;
if (property == null) {
// style for row
if (hasComplaints(customer)) {
return"unsatisfied-customer";
}
} else if (property.equals("grade")) {
// style for column "grade"
switch (customer.getGrade()) {
case PREMIUM: return "premium-grade";
case HIGH: return "high-grade";
case MEDIUM: return "medium-grade";
default: return null;
}
}
return null;
}
});
}
Далее нужно определить заданные для строк и ячеек стили в теме приложения. Подробная информация о создании темы находится в Создание темы приложения. Для веб-клиента новые стили определяются в файле styles.scss. Имена стилей, заданные в контроллере, совместно с префиксами, обозначающими строку или колонку таблицы, образуют CSS-селекторы. Например:
.v-table-row.unsatisfied-customer {
font-weight: bold;
}
.v-table-cell-content.premium-grade {
background-color: red;
}
.v-table-cell-content.high-grade {
background-color: green;
}
.v-table-cell-content.medium-grade {
background-color: blue;
}
ordersTable.addPrintable("customer", new Table.Printable<Customer, String>() {
@Override
public String getValue(Customer customer) {
return "Name: " + customer.getName;
}
});
Метод getValue() интерфейса Table.Printable должен возвращать данные, которые будут находиться в ячейке таблицы. Это может быть не только строка - метод может возвращать значения других типов, например, числовые данные или даты, и они будут представлены в XLS-файле соответствующим образом.
Если форматированный вывод в XLS необходим для генерируемой колонки, нужно использовать реализацию интерфейса Table.PrintableColumnGenerator, передавая ее методу addGeneratedColumn(). Значение для вывода в ячейку XLS-документа задается в методе getValue() этого интерфейса:
ordersTable.addGeneratedColumn("product", new Table.PrintableColumnGenerator<Order, String>() {
@Override
public Component generateCell(Order entity) {
Label label = componentsFactory.createComponent(Label.NAME);
Product product = order.getProduct();
label.setValue(product.getName() + ", " + product.getCost());
return label;
}
@Override
public String getValue(Order entity) {
Product product = order.getProduct();
return product.getName() + ", " + product.getCost();
}
});
Если генерируемой колонке тем или иным способом не задано представления Printable, то в случае, если колонке соответствует атрибут сущности, будет выведено его значение, в противном случае не будет выведено ничего.
Если такое действие найдено и имеет свойство enabled = true, оно выполняется.
Если такое действие найдено и имеет свойство enabled = true, оно выполняется.
Атрибуты table
align - aggregatable - aggregationStyle - columnControlVisible - columnHeaderVisible - contextMenuEnabled - editable - enable - height - id - multiLineCells - multiselect - presentations - reorderingAllowed - settingsEnabled - showSelection - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
Элементы table
actions - buttonsPanel - columns - rows - rowsCount
Атрибуты column
align - caption - captionProperty - collapsed - dateFormat - editable - generator - id - link - linkInvoke - linkScreen - linkScreenOpenType - maxTextLength - optionsDatasource - resolution - sortable - visible - width
Элементы column
Атрибуты aggregation
Атрибуты rows
Предопределенные стили table
borderless - compact - no-header - no-horizontal-lines - no-stripes - no-vertical-lines - small
API
addGeneratedColumn - addPrintable - addColumnCollapseListener - applySettings - generateCell - getSelected - requestFocus - saveSettings - scrollTo - setClickListener - setEnterPressAction - setItemClickAction - setStyleProvider
Текстовая область − многострочное текстовое поле для редактирования текста.
XML-имя компонента: textArea
Компонент TextArea реализован для блоков Web Client и Desktop Client.
TextArea в основном повторяет функциональность TextField и имеет следующие специфические атрибуты:
<textArea id="textArea" cols="20" rows="5" caption="msg://name"/>
Значения width и height имеют приоритет над значениями cols и rows.
<textArea id="textArea" resizableDirection="BOTH"/>
Доступны следующие режимы изменения размера:
События изменения размеров области можно отслеживать с помощью слушателя ResizeListener, например:
textArea.addResizeListener(e -> showNotification("Resized"));
Компонент TextArea поддерживает слушатель TextChangeListener, определённый в родительском интерфейсе TextInputField. События изменения текста обрабатываются асинхронно после ввода, не блокируя сам ввод.
textArea.addTextChangeListener(event -> {
int length = event.getText().length();
textAreaLabel.setValue(length + " of " + textArea.getMaxLength());
});
Параметром TextChangeEventMode задаётся режим передачи изменений на сервер для вызова события на серверной стороне. В платформе реализовано 3 режима передачи:
В случае, если ValueChangeEvent может случиться до истечения периода ожидания, событие TextChangeEvent обрабатывается до его истечения, при условии, что набранный текст был изменён после предыдущего TextChangeEvent.
Стили компонента TextArea
В веб-клиенте с темой, основанной на Halo, к компоненту TextArea можно применить предопределенные стили. Стили задаются в XML-дексрипторе или контроллере экрана с помощью атрибута stylename:
<textArea id="textArea"
stylename="borderless"/>
Чтобы применить стиль программно, выберите одну из констант класса HaloTheme с префиксом компонента TEXTAREA_:
textArea.setStyleName(HaloTheme.TEXTAREA_BORDERLESS);
Атрибуты textArea
align - caption - caseConversion - cols - contextHelpText - contextHelpTextHtmlEnabled - datasource - datatype - description - editable - enable - height - icon - id - maxLength - property - required - requiredMessage - resizableDirection - rows - settingsEnabled - stylename - tabIndex - trim - visible - width - wordwrap
Предопределенные стили textArea
align-center - align-right - borderless - huge - large - small - tiny
API
addResizeListener - addTextChangeListener - addValueChangeListener - applySettings - commit - discard - isModified - saveSettings - setContextHelpIconClickHandler
Поле для редактирования текста. Может использоваться как для работы с атрибутами сущностей, так и для ввода и отображения произвольной текстовой информации.
XML-имя компонента: textField
Компонент текстового поля реализован для блоков Web Client и Desktop Client.
<textField id="nameField" caption="msg://name"/>
На рисунке ниже показан вид простого текстового поля.
<textField id="textField"
stylename="borderless"/>
Чтобы применить стиль программно, выберите одну из констант класса HaloTheme с префиксом компонента TEXTFIELD_:
textField.setStyleName(HaloTheme.TEXTFIELD_INLINE_ICON);
Стили компонента TextField:
Компонент TextField поддерживает автоматическую конвертацию регистра. Атрибут caseConvertion может принимать следующие значения:
<dsContext>
<datasource id="customerDs" class="com.sample.sales.entity.Customer" view="_local"/>
</dsContext>
<layout>
<textField datasource="customerDs" property="name" caption="msg://name"/>
Как видно из примера, в экране описывается источник данных customerDs для некоторой сущности Покупатель (Customer), имеющей атрибут name. В компоненте текстового поля в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в текстовом поле.
В качестве примера рассмотрим текстовое поле с типом данных Integer.
<textField id="integerField" datatype="int" caption="msg://integerFieldName"/>
Если в таком поле ввести значение, которое невозможно интерпретировать как целое число, то при потере фокуса полем будет выведено сообщение об ошибке и значение поля вернется на предыдущее.
public class PositiveIntegerValidator implements Field.Validator {
@Override
public void validate(Object value) throws ValidationException {
Integer i = (Integer) value;
if (i <= 0)
throw new ValidationException("Value must be positive");
}
}
и задать его для текстового поля с типом данных int в элементе validator:
<textField id="integerField" datatype="int">
<validator class="com.sample.sales.gui.PositiveIntegerValidator"/>
</textField>
В отличие от проверки вводимой строки на соответствие типу данных, валидация срабатывает не сразу при потере полем фокуса, а только при вызове у поля метода validate(). Это означает, что поле (и связанный с ним атрибут сущности) может некоторое время содержать значение, не удовлетворяющее условиям валидации (в приведенном примере неположительное число). Это не является проблемой, так как обычно поля редактирования с валидацией располагаются в экране редактирования, а он автоматически вызывает валидацию всех своих полей перед коммитом. Если же поле находится не в экране редактирования, то необходимо вызывать метод validate() поля в контроллере явно.
textField.addTextChangeListener(event -> {
int length = event.getText().length();
textFieldLabel.setValue(length + " of " + textField.getMaxLength());
});
textField.setTextChangeEventMode(TextInputField.TextChangeEventMode.LAZY);
В случае, если ValueChangeEvent может случиться до истечения периода ожидания, событие TextChangeEvent обрабатывается до его истечения, при условии, что набранный текст был изменён после предыдущего TextChangeEvent.
textField.addEnterPressListener(e -> showNotification("Enter pressed"));
textField.addValueChangeListener(e ->
showNotification("Before: " + e.getPrevValue() + ". After: " + e.getValue()));
Если текстовое поле не связано с атрибутом, либо для него не определено значение length, либо это значение нужно переопределить, то для ограничения максимальной длины вводимого текста можно использовать атрибут maxLength. Значение "-1" означает отсутствие ограничения. Например:
<textField id="shortTextField" maxLength="10"/>
Следует иметь в виду, что отсечение пробелов работает только при вводе нового значения. Если в значении связанного атрибута уже присутствуют пробелы, они будут отображаться, пока пользователь не изменит значение поля.
Атрибуты textField
align - caption - caseConversion - contextHelpText - contextHelpTextHtmlEnabled - datasource - datatype - description - editable - enable - height - icon - id - inputPrompt - maxLength - property - required - requiredMessage - stylename - tabIndex - trim - visible - width
Элементы textField
Предопределенные стили textField
align-center - align-right - borderless - huge - inline-icon - large - small - tiny
API
addEnterPressListener - addTextChangeListener - addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
Поле для отображения и ввода времени.
XML-имя компонента: timeField.
Компонент TimeField реализован для блоков Web Client и Desktop Client.
<dsContext>
<datasource id="orderDs" class="com.sample.sales.entity.Order" view="_local"/>
</dsContext>
<layout>
<timeField datasource="orderDs" property="deliveryTime"/>
Как видно из примера, в экране описывается источник данных orderDs для некоторой сущности Заказ (Order), имеющей атрибут deliveryTime. В компоненте ввода времени в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в поле.
Связанный атрибут сущности должен быть типа java.util.Date или java.sql.Time.
<timeField datasource="orderDs" property="createTs" showSeconds="true"/>
Атрибуты timeField
align - caption - contextHelpText - contextHelpTextHtmlEnabled - editable - enable - datasource - description - height - icon - id - property - required - requiredMessage - showSeconds - stylename - tabIndex - timeFormat - visible - width
Элементы timeField
API
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
Компонент TokenList представляет собой упрощенный вариант работы со списком сущностей: названия экземпляров располагаются в вертикальном или горизонтальном списке, добавление производится из выпадающего списка, удаление - с помощью кнопок, расположенных рядом с каждым экземпляром.
XML-имя компонента: tokenList
Компонент реализован для блоков Web Client и Desktop Client.
Пример описания компонента TokenList в XML-дескрипторе экрана:
<dsContext>
<datasource id="orderDs"
class="com.sample.sales.entity.Order"
view="order-edit">
<collectionDatasource id="productsDs" property="products"/>
</datasource>
<collectionDatasource id="allProductsDs"
class="com.sample.sales.entity.Product"
view="_minimal">
<query>select p from sales$Product p order by p.name</query>
</collectionDatasource>
</dsContext>
<layout>
<tokenList id="productsList" datasource="productsDs" inline="true" width="500px">
<lookup optionsDatasource="allProductsDs"/>
</tokenList>
Здесь в элементе dsContext определен вложенный источник данных productsDs, содержащий коллекцию входящих в состав заказа продуктов. Кроме того, определен источник данных allProductsDs, содержащий коллекцию всех продуктов, имеющихся в базе данных. Компонент TokenList с идентификатором productsList отображает содержимое источника данных productsDs, а также позволяет изменять эту коллекцию, добавляя в него экземпляры из источника данных allProductsDs.
Атрибуты tokenList:
Элементы tokenList:
Атрибуты элемента lookup:
<tokenList id="linesList"
datasource="orderItemsDs"
width="320px">
<lookup optionsDatasource="allItemsDs" inputPrompt="Choose an item" />
</tokenList>
@Override
public void init(Map<String, Object> params) {
if (WindowParams.MULTI_SELECT.getBool(getContext())) {
usersTable.setMultiSelect(true);
}
}
Слушатели tokenList:
Атрибуты tokenList
align - caption - captionProperty - clearEnabled - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - inline - position - simple - stylename - tabIndex - visible - width
Элементы tokenList
Атрибуты lookup
captionProperty - filterMode - inputPrompt - lookup - lookupScreen - multiselect - openType - optionsDatasource
Атрибуты button
API
addValueChangeListener - setContextHelpIconClickHandler - setItemClickListener
Компонент Tree предназначен для отображения иерархической структуры, представленной сущностями, содержащими ссылки на самих себя.
XML-имя компонента: tree
Компонент реализован для блоков Web Client и Desktop Client.
Для Tree в атрибуте datasource элемента treechildren должен быть указан hierarchicalDatasource. Объявление hierarchicalDatasource должно содержать атрибут hierarchyProperty - имя атрибута сущности, являющегося ссылкой на саму себя.
Пример описания компонента Tree в XML-дескрипторе экрана:
<dsContext>
<hierarchicalDatasource id="departmentsDs" class="com.sample.sales.entity.Department" view="browse"
hierarchyProperty="parentDept">
<query>
select d from sales$Department d order by d.createTs
</query>
</hierarchicalDatasource>
</dsContext>
<layout>
<tree id="departmentsTree" width="100%" height="100%">
<treechildren datasource="departmentsDs" captionProperty="name"/>
</tree>
В атрибуте captionProperty элемента treechildren можно задать имя свойства сущности, отображаемого в дереве. Если этот атрибут не определен, то будет отображаться имя экземпляра сущности.
Атрибут multiselect позволяет задать режим множественного выделения элементов дерева. Если multiselect равен true, то пользователь может выделить несколько элементов с помощью клавиатуры или мыши, удерживая клавиши Ctrl или Shift. По умолчанию режим множественного выделения отключен.
Метод setItemClickAction() позволяет задать действие, которое будет выполнено при двойном клике по узлу дерева.
Каждый элемент дерева может иметь значок слева. Создайте реализацию интерфейса ListComponent.IconProvider в контроллере экрана и установите ее для компонента Tree:
@Inject
private Tree<Region> tree;
@Override
public void init(Map<String, Object> params) {
tree.setIconProvider(new ListComponent.IconProvider<Region>() {
@Nullable
@Override
public String getItemIcon(Region entity) {
if (entity.getParent() == null) {
return "icons/root.png";
}
return "icons/leaf.png";
}
});
}
Атрибуты tree
enable - height - id - multiselect - stylename - tabIndex - visible - width
Элементы tree
Атрибуты treechildren
Компонент TreeTable − иерархическая таблица, отображающая в первой колонке древовидную структуру. Предназначена для работы с сущностями, которые содержат ссылки на самих себя. Это могут быть например, файловая система или организационная структура предприятия.
XML-имя компонента: treeTable
Компонент реализован для блоков Web Client и Desktop Client.
Для TreeTable в атрибуте datasource элемента rows должен быть указан hierarchicalDatasource. Объявление hierarchicalDatasource должно содержать атрибут hierarchyProperty - имя атрибута сущности, являющегося ссылкой на саму себя.
Пример описания таблицы в XML-дескрипторе экрана:
<dsContext>
<hierarchicalDatasource id="tasksDs" class="com.sample.sales.entity.Task" view="browse"
hierarchyProperty="parentTask">
<query>
select t from sales$Task t
</query>
</hierarchicalDatasource>
</dsContext>
<layout>
<treeTable id="tasksTable" width="100%">
<columns>
<column id="name"/>
<column id="dueDate"/>
<column id="assignee"/>
</columns>
<rows datasource="tasksDs"/>
</treeTable>
Функциональность TreeTable аналогична простой таблице Table.
Атрибуты treeTable
align - aggregatable - aggregationStyle - columnControlVisible - contextMenuEnabled - editable - enable - height - id - multiLineCells - multiselect - presentations - reorderingAllowed - settingsEnabled - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
Элементы treeTable
actions - buttonsPanel - columns - rows - rowsCount
Атрибуты column
align - caption - captionProperty - collapsed - dateFormat - editable - id - link - linkInvoke - linkScreen - linkScreenOpenType - maxTextLength - optionsDatasource - resolution - sortable - visible - width
Элементы column
Атрибуты rows
API
addColumnCollapseListener - setClickListener
Компонент TwinColumn представляет собой сдвоенный список для множественного выбора опций. В левом списке содержатся доступные невыбранные значения, в правом списке содержатся выбранные значения. Пользователь выбирает значения, перенося их из левого в правый список и обратно с помощью двойного клика или соответствующих кнопок. Для каждого значения можно задать уникальный стиль отображения и значок.
XML-имя компонента: twinColumn
Компонент реализован только для блока Web Client.
Пример использования компонента twinColumn для выбора экземпляров сущности:
<dsContext>
<datasource id="carDs" class="com.company.sample.entity.Car" view="_local"/>
<collectionDatasource id="coloursDs" class="com.company.sample.entity.Colour" view="_local">
<query>select c from sample$Colour c</query>
</collectionDatasource>
</dsContext>
<layout>
<twinColumn id="coloursField" optionsDatasource="coloursDs" addAllBtnEnabled="true"/>
В данном случае компонент coloursField отобразит имена экземпляров сущности Colour, находящихся в источнике данных coloursDs, а его метод getValue() вернет коллекцию выбранных экземпляров сущности.
Атрибут addAllBtnEnabled задает отображение кнопок, позволяющих перемещать между списками все опции сразу.
Атрибут columns используется для задания количества символов в строке, а атрибут rows − для задания количества строк текста в каждом списке.
Атрибуты leftColumnCaption и rightColumnCaption используются для назначения заголовков списков.
Для задания внешнего вида опций можно реализовать интерфейс TwinColumn.StyleProvider и возвращать название стиля и путь к значку в зависимости от конкретного экземпляра сущности, отображаемого в компоненте.
Список опций компонента TwinColumn может быть задан произвольно с помощью методов setOptionsList(), setOptionsMap() и setOptionsEnum(), аналогично описанному для компонента OptionsGroup.
Атрибуты twinColumn
align - addAllBtnEnabled - caption - captionProperty - columns - contextHelpText - contextHelpTextHtmlEnabled - datasource - description - editable - enable - height - icon - id - leftColumnCaption - optionsDatasource - property - required - requiredMessage - rightColumnCaption - rows - stylename - tabIndex - visible - width
Элементы twinColumn
API
addValueChangeListener - setContextHelpIconClickHandler
Контейнер Accordion - это вертикальный контейнер со сворачиваемыми вкладками, который позволяет легко скрывать и отображать большой объем контента. Accordion реализован для блока Web Client.
XML-имя компонента: accordion. Пример описания аккордеона в XML-дескрипторе экрана:
<accordion id="accordion" height="100%">
<tab id="tabStamford" caption="msg://tabStamford" margin="true" spacing="true">
<label value="msg://sampleStamford"/>
</tab>
<tab id="tabBoston" caption="msg://tabBoston" margin="true" spacing="true">
<label value="msg://sampleBoston"/>
</tab>
<tab id="tabLondon" caption="msg://tabLondon" margin="true" spacing="true">
<label value="msg://sampleLondon"/>
</tab>
</accordion>
Компонент accordion должен иметь вложенные элементы tab, описывающие вкладки. Каждая вкладка является контейнером с вертикальным расположением компонентов, аналогичным vbox. Контейнер аккордеон может быть использован при нехватке места на странице приложения, или же если название вкладки слишком длинное для отображения в TabSheet. Аккордеон предоставляет анимацию плавного перехода.
Атрибуты элемента tab:
При открытии экрана lazy-вкладки не загружают свое содержимое, что приводит к созданию меньшего количества компонентов в памяти. Компоненты вкладки загружаются только в тот момент, когда пользователь выбирает данную вкладку. Кроме того, если на lazy-вкладке расположены визуальные компоненты, связанные с источником данных, содержащим JPQL запрос, то этот запрос также не выполняется. В результате экран открывается быстрее, а данные загружаются только в тот момент, когда пользователь действительно хочет их увидеть, выбирая данную вкладку.
Следует иметь в виду, что компоненты, расположенные на lazy-вкладке, не существуют в момент открытия экрана. Поэтому их нельзя инжектировать в контроллер, и нельзя получить вызовом getComponent() в методе init() контроллера. Обратиться к компонентам lazy-вкладки можно только после того, как пользователь на нее переключился. Этот момент можно отловить с помощью слушателя Accordion.SelectedTabChangeListener, например:
@Inject
private Accordion accordion;
private boolean tabInitialized;
@Override
public void init(Map<String, Object> params) {
accordion.addSelectedTabChangeListener(event -> {
if ("tabCambridge".equals(event.getSelectedTab().getName())) {
initCambridgeTab();
}
});
}
private void initCambridgeTab(){
if (tabInitialized) {
return;
}
tabInitialized = true;
// initialization code here
// use getComponentNN("comp_id") here to get lazy tab's components
}
По умолчанию вкладки не являются lazy, а значит, загружают свое содержимое в момент открытия экрана.
accordion.setStyleName(HaloTheme.ACCORDION_BORDERLESS);
Вкладка компонента accordion может содержать любой другой визуальный контейнер, такой как таблица, сетка и т.д.:
<accordion id="accordion" height="100%" width="100%" enable="true">
<tab id="tabNY" caption="msg://tabNY" margin="true" spacing="true">
<table id="nYTable" width="100%">
<columns>
<column id="borough"/>
<column id="county"/>
<column id="population"/>
<column id="square"/>
</columns>
<rows datasource="newYorkDs"/>
</table>
</tab>
</accordion>
Атрибуты accordion
colspan - enable - height - id - rowspan - stylename - tabCaptionsAsHtml - tabIndex - visible - width
Атрибуты tab
caption - description - enable - expand - icon - id - lazy - margin - spacing - stylename - visible
API
add - addSelectedTabChangeListener - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - remove - removeAll
BoxLayout представляет собой контейнер с последовательным размещением компонентов.
Существует три типа BoxLayout, определяемых именем XML-элемента:
<hbox spacing="true" margin="true">
<dateField datasource="orderDs" property="date"/>
<lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs"/>
<textField datasource="orderDs" property="amount"/>
</hbox>
<vbox spacing="true" margin="true">
<dateField datasource="orderDs" property="date"/>
<lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs"/>
<textField datasource="orderDs" property="amount"/>
</vbox>
<flowBox spacing="true" margin="true">
<dateField datasource="orderDs" property="date"/>
<lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs"/>
<textField datasource="orderDs" property="amount"/>
</flowBox>
В веб-клиенте с темой, основанной на Halo, BoxLayout может быть использован для создания сложных составных компонентов. Атрибут stylename со значением card или well в сочетании с атрибутом stylename="v-panel-caption" вложенного контейнера задают компоненту внешний вид Vaadin Panel.
<vbox stylename="well"
height="200px"
width="300px"
expand="message"
spacing="true">
<hbox stylename="v-panel-caption"
width="100%">
<label value="Widget caption"/>
<button align="MIDDLE_RIGHT"
icon="font-icon:EXPAND"
stylename="borderless-colored"/>
</hbox>
<textArea id="message"
inputPrompt="Enter your message here..."
width="280"
align="MIDDLE_CENTER"/>
<button caption="Send message"
width="100%"/>
</vbox>
Метод getComponent() позволяет получить дочерний компонент BoxLayout по его индексу:
Button button = (Button) hbox.getComponent(0);
В компоненте BoxLayout можно использовать горячие клавиши. Задать сочетание клавиш и вызываемое действие можно с помощью метода addShortcutAction():
flowBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
Атрибуты hbox, vbox, flowBox
align - enable - expand - height - id - margin - spacing - stylename - visible - width
API
add - addLayoutClickListener - addShortcutAction - getComponent - getComponentNN - getComponents - getMargin - getOwnComponent - getOwnComponents - indexOf - remove - removeAll - setMargin - setSpacing
ButtonsPanel - контейнер, унифицирующий использование и размещение компонентов (чаще всего кнопок) для управления данными в таблице.
XML-имя компонента: buttonsPanel.
Пример описания ButtonsPanel в XML-дескрипторе экрана:
<table id="customersTable"
editable="false" width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
<action id="excel"/>
</actions>
<buttonsPanel>
<button action="customersTable.create"/>
<button action="customersTable.edit"/>
<button action="customersTable.remove"/>
<button action="customersTable.excel"/>
</buttonsPanel>
<columns>
<column id="name"/>
<column id="email"/>
</columns>
<rows datasource="customersDs"/>
</table>
Элемент buttonsPanel можно разместить как внутри table, так и в произвольном месте экрана.
Если buttonsPanel находится внутри table, то она комбинируется с компонентом rowsCount таблицы, тем самым оптимально расходуя место по вертикали. Кроме того, в этом случае при открытии экрана выбора методом Frame.openLookup() (например, из компонента PickerField) панель кнопок скрывается.
Атрибут alwaysVisible служит для отключения скрытия панели в экране выбора при его открытии методом Frame.openLookup(). Если значение атрибута равно true, то панель с кнопками не скрывается. По умолчанию значение атрибута равно false.
События щелчка по области компонента buttonsPanel можно отслеживать с помощью интерфейса LayoutClickListener.
В компоненте ButtonsPanel можно использовать горячие клавиши. Задать сочетание клавиш и вызываемое действие можно с помощью метода addShortcutAction():
buttonsPanel.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
Атрибуты buttonsPanel
align - alwaysVisible - enable - expand - height - id - stylename - visible - width
API
addLayoutClickListener - addShortcutAction - setSpacing
Контейнер CssLayout позволяет управлять размещением и стилизацией своих компонентов с помощью CSS.
XML-имя компонента: cssLayout.
Ниже приведен пример использования cssLayout в простом responsive экране.
Отображение компонентов на широком дисплее:
Отображение компонентов на узком дисплее:
XML-дескриптор экрана:
<cssLayout responsive="true"
stylename="responsive-container"
width="100%">
<vbox margin="true"
spacing="true"
stylename="group-panel">
<textField caption="Field One" width="100%"/>
<textField caption="Field Two" width="100%"/>
<button caption="Button"/>
</vbox>
<vbox margin="true"
spacing="true"
stylename="group-panel">
<textField caption="Field Three" width="100%"/>
<textField caption="Field Four" width="100%"/>
<button caption="Button"/>
</vbox>
</cssLayout>
Содержимое файла modules/web/themes/halo/halo-ext.scss (в разделе Расширение существующей темы приведена информация о том как создать этот файл):
/* Define your theme modifications inside next mixin */
@mixin halo-ext {
@include halo;
.responsive-container {
&[width-range~="0-900px"] {
.group-panel {
width: 100% !important;
}
}
&[width-range~="901px-"] {
.group-panel {
width: 50% !important;
}
}
}
}
<cssLayout stylename="v-component-group">
<textField inputPrompt="Search..."/>
<button caption="OK"/>
</cssLayout>
<cssLayout height="300px"
stylename="card"
width="300px">
<hbox stylename="v-panel-caption"
width="100%">
<label value="Widget caption"/>
<button align="MIDDLE_RIGHT"
icon="font-icon:EXPAND"
stylename="borderless-colored"/>
</hbox>
<vbox height="100%">
<label value="Panel content"/>
</vbox>
</cssLayout>
Результат:
Атрибуты cssLayout
enable - height - id - responsive - stylename - visible - width
API
add - addShortcutAction - addLayoutClickListener - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - indexOf - remove - removeAll
Элемент frame предназначен для включения в экран фреймов.
Атрибуты:
Должен быть указан один из этих атрибутов. Если указано оба, фрейм будет загружен из явно указанного в src файла.
Атрибуты frame
align - height - id - screen - src - stylename - visible - width
API
add - getComponent - getComponentNN - getComponents - getMargin - getOwnComponent - getOwnComponents - indexOf - remove - removeAll - setMargin - setSpacing
GridLayout - контейнер, располагающий компоненты по сетке.
XML-имя компонента: grid.
Пример использования контейнера:
<grid spacing="true">
<columns count="4"/>
<rows>
<row>
<label value="Date" align="MIDDLE_LEFT"/>
<dateField datasource="orderDs" property="date"/>
<label value="Customer" align="MIDDLE_LEFT"/>
<lookupField datasource="orderDs" property="customer"
optionsDatasource="customersDs"/>
</row>
<row>
<label value="Amount" align="MIDDLE_LEFT"/>
<textField datasource="orderDs" property="amount"/>
</row>
</rows>
</grid>
Элементы grid:
В простейшем случае достаточно задать число колонок в атрибуте count. Тогда, если ширина всего контейнера явно задана в пикселах или процентах, незанятое место будет распределяться между колонками равными долями.
Для распределения незанятого места неравными долями необходимо определить для каждой колонки элемент column и задать для него атрибут flex.
Пример сетки, в которой вторая и четвертая колонки занимают все лишнее место по горизонтали, причем четвертая колонка забирает себе в три раза больше лишнего места:
<grid spacing="true" width="100%">
<columns>
<column/>
<column flex="1"/>
<column/>
<column flex="3"/>
</columns>
<rows>
<row>
<label value="Date"/>
<dateField datasource="orderDs" property="date" width="100%"/>
<label value="Customer"/>
<lookupField datasource="orderDs" property="customer"
optionsDatasource="customersDs" width="100%"/>
</row>
<row>
<label value="Amount"/>
<textField datasource="orderDs" property="amount" width="100%"/>
</row>
</rows>
</grid>
Если атрибут flex не указан, или указано значение 0, то ширина данной колонки будет установлена по содержимому, если хотя бы одна другая колонка имеет ненулевой flex. В приведенном примере первая и третья колонки получат ширину по максимальной длине текста надписей.
|
Для того, чтобы лишнее место вообще образовалось, необходимо установить всему контейнеру ширину в пикселах или процентах. В противном случае ширина колонок будет рассчитана по ширине содержимого, и атрибут flex не будет иметь никакого эффекта. |
Элемент row может содержать атрибут flex, аналогичный описанному для column, но влияющий на распределение лишнего места по вертикали при заданной общей высоте сетки.
Элемент row должен содержать элементы компонентов, отображаемых в ячейках данной строки сетки. Число компонентов в одной строке не должно превышать заданного количества колонок, но может быть меньше.
Любой компонент, находящийся в контейнере grid, может иметь атрибуты colspan и rowspan. Эти атрибуты задают соответственно сколько колонок и строк будет занимать данный компонент. Например, так можно растянуть поле Field3 на три колонки:
<grid spacing="true">
<columns count="4"/>
<rows>
<row>
<label value="Name 1"/>
<textField/>
<label value="Name 2"/>
<textField/>
</row>
<row>
<label value="Name 3"/>
<textField colspan="3" width="100%"/>
</row>
</rows>
</grid>
В результате компоненты будут располагаться следующим образом:
События щелчка по области компонента GridLayout можно отслеживать с помощью интерфейса LayoutClickListener.
Метод getComponent() позволяет получить дочерний компонент GridLayout по индексам его колонки и строки:
Button button = (Button) gridLayout.getComponent(0,1);
В компоненте GridLayout можно использовать горячие клавиши. Задать сочетание клавиш и вызываемое действие можно с помощью метода addShortcutAction():
grid.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
Атрибуты grid
align - enable - height - id - margin - spacing - stylename - visible - width
Элементы grid
Атрибуты columns
Атрибуты column
Атрибуты row
API
add - addShortcutAction - addLayoutClickListener - getComponent - getComponentNN - getComponents - getMargin - getOwnComponent - getOwnComponents - remove - removeAll - setMargin - setSpacing
GroupBoxLayout - контейнер, позволяющий выделить рамкой содержащиеся в нем компоненты, и задать им общий заголовок. Кроме того, он умеет сворачивать свое содержимое.
XML-имя компонента: groupBox.
Пример описание контейнера в XML-дескрипторе экрана:
<groupBox caption="Order">
<dateField datasource="orderDs" property="date" caption="Date"/>
<lookupField datasource="orderDs" property="customer"
optionsDatasource="customersDs" caption="Customer"/>
<textField datasource="orderDs" property="amount" caption="Amount"/>
</groupBox>
Атрибуты groupBox:
/
. Пример свернутого GroupBox:
Изменения состояния компонента groupBox (сворачивание и разворачивание) можно отслеживать с помощью интерфейса ExpandedStateChangeListener.
<groupBox outerMargin="true, false, true, false">
Если атрибут showAsPanel установлен в true, outerMargin игнорируется.
Контейнер groupBox по умолчанию имеет ширину 100% аналогично vbox.
В веб-клиенте с темой, основанной на Halo, к компоненту groupBox можно применить предопределенные стили. Стили задаются в XML-дексрипторе или контроллере экрана с помощью атрибута stylename. Чтобы применить стиль программно, выберите одну из констант класса HaloTheme с префиксом компонента LAYOUT_ или GROUPBOX_. Следующие стили должны использоваться совместно с атрибутом showAsPanel, имеющим значение true:
groupBox.setShowAsPanel(true);
groupBox.setStyleName(HaloTheme.GROUPBOX_PANEL_BORDERLESS);
<groupBox caption="Well-styled groupBox"
showAsPanel="true"
stylename="well"
width="300px"
height="200px"/>
В компоненте Groupbox можно использовать горячие клавиши. Задать сочетание клавиш и вызываемое действие можно с помощью метода addShortcutAction():
groupBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
Атрибуты groupBox
align - caption - collapsable - collapsed - expand - height - id - orientation - outerMargin - settingsEnabled - spacing - stylename - width
Предопределенные стили groupBox
borderless - card - well
API
add - addExpandedStateChangeListener - addShortcutAction - applySettings - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - indexOf - remove - removeAll - saveSettings - setOuterMargin - setSpacing
HtmlBoxLayout позволяет определять расположение компонентов в HTML-шаблоне, который включается в тему.
|
Не используйте HtmlBoxLayout для отображения динамического содержимого или для встраивания кода JavaScript. Для этих целей лучше использовать компонент BrowserFrame. |
XML-имя компонента: htmlBox.
Ниже приведен пример использования htmlBox в простом экране.
XML-дескриптор экрана:
<htmlBox align="TOP_CENTER"
template="sample"
width="500px">
<label id="logo"
value="Subscribe"
stylename="logo"/>
<textField id="email"
width="100%"
inputPrompt="email@test.test"/>
<button id="submit"
width="100%"
invoke="showMessage"
caption="Subscribe"/>
</htmlBox>
Атрибуты htmlBox:
Например, если вы используете тему Halo и хотите назвать шаблон my_template, укажите my_template в атрибуте и разместите шаблон в файле modules/web/themes/halo/layouts/my_template.html.
Содержимое шаблона modules/web/themes/halo/layouts/sample.html:
<div location="logo" class="logo"></div>
<table class="component-container">
<tr>
<td>
<div location="email" class="email"></div>
</td>
<td>
<div location="submit" class="submit"></div>
</td>
</tr>
</table>
Шаблон должен содержать элементы <div> с атрибутами location. В этих элементах будут отображаться компоненты CUBA, определенные в XML дескрипторе с соответствующими идентификаторами.
Содержимое файла modules/web/themes/halo/com.company.application/halo-ext.scss (в разделе Расширение существующей темы приведена информация о том как создать этот файл):
@mixin com_company_application-halo-ext {
.email {
width: 390px;
}
.submit {
width: 100px;
}
.logo {
font-size: 96px;
text-transform: uppercase;
margin-top: 50px;
}
.component-container {
display: inline-block;
vertical-align: top;
width: 100%;
}
}
Пример использования атрибута:
<htmlBox height="256px"
width="400px">
<templateContents>
<![CDATA[
<table align="center" cellspacing="10"
style="width: 100%; height: 100%; color: #fff; padding: 20px; background: #31629E repeat-x">
<tr>
<td colspan="2"><h1 style="margin-top: 0;">Login</h1>
<td>
</tr>
<tr>
<td align="right">User name:</td>
<td>
<div location="username"></div>
</td>
</tr>
<tr>
<td align="right">Password:</td>
<td>
<div location="password"></div>
</td>
</tr>
<tr>
<td align="right" colspan="2">
<div location="okbutton" style="padding: 10px;"></div>
</td>
</tr>
<tr>
<td colspan="2" style="padding: 7px; background-color: #4172AE"><span
style="font-family: FontAwesome; margin-right: 5px;"></span> This information is in the layout.
<td>
</tr>
</table>
]]>
</templateContents>
<textField id="username"
width="100%"/>
<textField id="password"
width="100%"/>
<button id="okbutton"
caption="Login"/>
</htmlBox>
Атрибуты htmlBox
align - enable - height - id - stylename - template - templateContents - visible - width
API
add - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - remove - removeAll
ScrollBoxLayout − контейнер, который позволяет прокручивать свое содержимое.
XML-имя компонента: scrollBox
Пример описание контейнера с прокруткой в XML-дескрипторе экрана:
<groupBox caption="Order" width="300" height="170">
<scrollBox width="100%" height="100%" spacing="true" margin="true">
<dateField datasource="orderDs" property="date" caption="Date"/>
<lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs" caption="Customer"/>
<textField datasource="orderDs" property="amount" caption="Amount"/>
</scrollBox>
</groupBox>
|
Вложенные в scrollBox компоненты должны иметь фиксированные размеры или размеры по умолчанию. Нельзя устанавливать height="100%" или width="100%". В то же время scrollBox не может вычислять свои собственные размеры по содержимому. Ему нужно либо указать абсолютные размеры, либо растянуть в родительском контейнере, установив height="100%" и width="100%". |
В компоненте ScrollBox можно использовать горячие клавиши. Задать сочетание клавиш и вызываемое действие можно с помощью метода addShortcutAction():
scrollBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
Атрибуты scrollBox
align - height - id - margin - orientation - scrollBars - spacing - stylename - width
API
add - addShortcutAction - getComponent - getComponentNN - getComponents - getMargin - getOwnComponent - getOwnComponents - indexOf - remove - removeAll - setMargin - setSpacing
SplitPanel − контейнер, разбитый на две области, размер которых по горизонтали либо вертикали можно менять путем перемещения разделителя.
XML-имя компонента: split.
Пример описания панели с разделителем в XML-дескрипторе экрана:
<split orientation="horizontal" pos="30" width="100%" height="100%">
<vbox margin="true" spacing="true">
<dateField datasource="orderDs" property="date" caption="Date"/>
<lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs" caption="Customer"/>
</vbox>
<vbox margin="true" spacing="true">
<textField datasource="orderDs" property="amount" caption="Amount"/>
</vbox>
</split>
Внутри контейнера split обязательно должны находиться два вложенных контейнера или компонента, которые и будут расположены по обе стороны разделителя.
Атрибуты split:
|
Сворачивание доступно только для горизонтального контейнера SplitPanel. |
<split orientation="horizontal"
dockable="true"
dockMode="RIGHT">
...
</split>
Например, вы можете запретить перетаскивать сплиттер вне диапазона между 100 и 300 пикселями с левой стороны компонента:
<split id="splitPanel" maxSplitPosition="300px" minSplitPosition="100px" width="100%" height="100%">
<vbox margin="true" spacing="true">
<button caption="Button 1"/>
<button caption="Button 2"/>
</vbox>
<vbox margin="true" spacing="true">
<button caption="Button 4"/>
<button caption="Button 5"/>
</vbox>
</split>
Если вы хотите установить диапазон программно, вы должны указать единицу измерения с помощью Component.UNITS_PIXELS или Component.UNITS_PERCENTAGE
splitPanel.setMinSplitPosition(100, Component.UNITS_PIXELS);
splitPanel.setMaxSplitPosition(300, Component.UNITS_PIXELS);
split.setStyleName(HaloTheme.SPLITPANEL_LARGE);
Методы SplitPanel:
Атрибуты split
align - dockable - dockMode - height - id - locked - minSplitPosition - maxSplitPosition - orientation - pos - reversePosition - settingsEnabled - stylename - width
API
add - addPositionUpdateListener - applySettings - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - remove - removeAll - saveSettings
Контейнер TabSheet - это панель с вкладками (tabs). В один момент времени отображается содержимое только одной вкладки.
XML-имя компонента: tabSheet.
Пример описания панели с вкладками в XML-дескрипторе экрана:
<tabSheet>
<tab id="mainTab" caption="Tab1" margin="true" spacing="true">
<dateField datasource="orderDs" property="date" caption="Date"/>
<lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs" caption="Customer"/>
</tab>
<tab id="additionalTab" caption="Tab2" margin="true" spacing="true">
<textField datasource="orderDs" property="amount" caption="Amount"/>
</tab>
</tabSheet>
Атрибут description контейнера tabSheet задаёт текст подсказки, отображаемой при наведении курсора мыши или клике в области вкладок контейнера.
Компонент tabSheet должен иметь вложенные элементы tab, описывающие вкладки. Каждая вкладка является контейнером с вертикальным расположением компонентов, аналогичным vbox.
Атрибуты элемента tab:
При открытии экрана lazy-вкладки не загружают свое содержимое, что приводит к созданию меньшего количества компонентов в памяти. Компоненты вкладки загружаются только в тот момент, когда пользователь выбирает данную вкладку. Кроме того, если на lazy-вкладке расположены визуальные компоненты, связанные с источником данных, содержащим JPQL запрос, то этот запрос также не выполняется. В результате экран открывается быстрее, а данные загружаются только в тот момент, когда пользователь действительно хочет их увидеть, выбирая данную вкладку.
Следует иметь в виду, что компоненты, расположенные на lazy-вкладке, не существуют в момент открытия экрана. Поэтому их нельзя инжектировать в контроллер, и нельзя получить вызовом getComponent() в методе init() контроллера. Обратиться к компонентам lazy-вкладки можно только после того, как пользователь на нее переключился. Этот момент можно отловить с помощью слушателя TabSheet.SelectedTabChangeListener, например:
@Inject
private TabSheet tabsheet;
private boolean detailsInitialized, historyInitialized;
@Override
public void init(Map<String, Object> params) {
tabsheet.addSelectedTabChangeListener(event -> {
if ("detailsTab".equals(event.getSelectedTab().getName())) {
initDetails();
} else if ("historyTab".equals(event.getSelectedTab().getName())) {
initHistory();
}
});
}
private void initDetails(){
if (detailsInitialized) {
return;
}
// use getComponentNN("comp_id") here to get tab's components
detailsInitialized=true;
}
private void initHistory(){
if (historyInitialized) {
return;
}
// use getComponentNN("comp_id") here to get tab's components
historyInitialized = true;
}
По умолчанию вкладки не являются lazy, а значит, загружают свое содержимое в момент открытия экрана.
Стили TabSheet
В веб-клиенте с темой, основанной на Halo, к контейнеру TabSheet можно применить предопределенные стили. Стили задаются в XML-дексрипторе или контроллере экрана с помощью атрибута stylename:
<tabSheet stylename="framed">
<tab id="mainTab" caption="Framed tab"/>
</tabSheet>
Чтобы применить стиль программно, выберите одну из констант класса HaloTheme с префиксом компонента TABSHEET_:
tabSheet.setStyleName(HaloTheme.TABSHEET_COMPACT_TABBAR);
Атрибуты tabSheet
height - id - description - stylename - tabCaptionsAsHtml - tabIndex - tabsVisible - visible - width
Атрибуты tab
caption - closable - description - detachable - enable - expand - margin - icon - id - lazy - spacing - stylename - visible
Предопределенные стили tabSheet
centered-tabs - compact-tabbar - equal-width-tabs - framed - framed - only-selected-closeable - padded-tabbar
API
add - addSelectedTabChangeListener - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - remove - removeAll
В данном разделе рассматриваются различные элементы универсального пользовательского интерфейса, имеющие отношение к визуальным компонентам.
Formatter предназначен для преобразования некоторого значения в строку.
|
Formatter предназначен для использования с read-only компонентами, такими как Label, колонка Table и тому подобными. Для форматирования значения в редактируемых компонентах, например TextField, используйте механизм Datatype. |
В XML-дескрипторе экрана formatter для компонента может быть задан во вложенном элементе formatter. Элемент имеет единственный атрибут:
Если конструктор класса formatter принимает параметр типа org.dom4j.Element, то ему будет передан элемент XML, описывающий данный formatter. Это можно использовать для параметризации экземпляра formatter’а, например, строкой форматирования. В частности, имеющиеся в платформе классы DateFormatter и NumberFormatter могут брать строку форматирования из атрибута format. Пример использования:
<column id="date">
<formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" format="yyyy-MM-dd HH:mm:ss"/>
</column>
Кроме того, класс DateFormatter распознает также атрибут type, который может принимать значения DATE или DATETIME. В этом случае форматирование производится с помощью механизма Datatype по строке формата dateFormat или dateTimeFormat соответственно. Например:
<column id="endDate">
<formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" type="DATE"/>
</column>
По умолчанию, DateFormatter отображает дату и время в часовом поясе сервера. Чтобы учитывать часовой пояс текущего пользователя, установите значение true для атрибута useUserTimezone элемента formatter.
|
Если formatter реализован внутренним классом, то он должен быть объявлен с модификатором static, а его имя для загрузки отделяется символом "$", например: <formatter class="com.sample.sales.gui.OrderBrowse$CurrencyFormatter"/> |
Formatter можно назначить компоненту не только в XML-дескрипторе экрана, но и программно, передавая экземпляр formatter’а в метод setFormatter() компонента.
Пример объявления собственного formatter’а и использования его для форматирования значения колонки таблицы:
public class CurrencyFormatter implements Formatter<BigDecimal> {
protected GeneralConfiguration generalConfiguration;
protected Currency currentCurrency;
public CurrencyFormatter(GeneralConfiguration generalConfiguration) {
this.generalConfiguration = generalConfiguration;
currentCurrency = generalConfiguration.getCurrency();
}
@Override
public String format(BigDecimal value) {
return currentCurrency.format(value);
}
}
protected void initTableColumns() {
Formatter<BigDecimal> currencyFormatter = new CurrencyFormatter(generalConfiguration);
table.getColumn("totalPrice").setFormatter(currencyFormatter);
}
Механизм представлений позволяет пользователям системы управлять настройками отображения таблиц.
Пользователи могут:
Представления доступны в компонентах, реализующих интерфейс com.haulmont.cuba.gui.components.Component.HasPresentations. В платформе такими компонентами являются:
Таймер − это невизуальный компонент, позволяющий выполнять некоторый код контроллера экрана через определенные промежутки времени. Срабатывание таймера происходит в потоке обработки событий пользовательского интерфейса, что позволяет обновлять экран без каких-либо ограничений. Таймер прекращает работу при закрытии экрана, для которого он был создан.
Компонент реализован для блоков Web Client и Desktop Client. Для веб-клиента реализация таймеров основана на опросе сервера из веб-браузера, для десктоп клиента - на javax.swing.Timer.
Основной способ создания таймеров - декларативно в XML-дескрипторе экрана в элементе timers, располагающемся между элементами dsContext и layout.
Для описания таймера используется элемент timer.
Пример использования таймера для периодического обновления содержимого таблицы:
<window ...
<dsContext>
<collectionDatasource id="bookInstanceDs" ...
</dsContext>
<timers>
<timer delay="3000" autostart="true" repeating="true" onTimer="refreshData"/>
</timers>
<layout ...
@Inject
private CollectionDatasource bookInstanceDs;
public void refreshData(Timer timer) {
bookInstanceDs.refresh();
}
Таймер можно инжектировать в поле контроллера, либо получить методом Window.getTimer(). Управлять активностью таймера можно с помощью его методов start() и stop(). Для уже активного таймера вызов start() игнорируется. После остановки таймера методом stop() его можно снова запустить методом start().
Пример определения таймера в XML дескрипторе и использования листенеров в контроллере:
<timers>
<timer id="helloTimer" delay="5000"/>
</timers>
@Inject
private Timer helloTimer;
@Override
public void init(Map<String, Object> params) {
// add execution handler
helloTimer.addActionListener(timer -> {
showNotification("Hello", NotificationType.HUMANIZED);
});
// add stop listener
helloTimer.addStopListener(timer -> {
showNotification("Timer is stopped", NotificationType.HUMANIZED);
});
// start the timer
helloTimer.start();
}
Таймер можно также создавать в коде контроллера, при этом необходимо явно добавить таймер к экрану с помощью метода addTimer(), к примеру:
@Inject
private ComponentsFactory componentsFactory;
@Override
public void init(Map<String, Object> params) {
// create timer
Timer helloTimer = componentsFactory.createTimer();
// add timer to the screen
addTimer(helloTimer);
// set timer parameters
helloTimer.setDelay(5000);
helloTimer.setRepeating(true);
// add execution handler
helloTimer.addActionListener(timer -> {
showNotification("Hello", NotificationType.HUMANIZED);
});
// add stop listener
helloTimer.addStopListener(timer -> {
showNotification("Timer is stopped", NotificationType.HUMANIZED);
});
// start the timer
helloTimer.start();
}
Валидатор предназначен для проверки значения, введенного в визуальном компоненте.
|
Следует отличать валидацию от проверки типа данных. Если для некоторого компонента, например TextField, задан тип, отличный от строкового (это происходит при связывании с атрибутом сущности или назначении datatype), то компонент не позволяет ввести значение, не удовлетворяющее этому типу - при потере фокуса или нажатии Enter компонент отобразит предыдущее значение. Валидация же срабатывает не сразу при вводе или потере компонентом фокуса, а только при вызове у компонента метода validate(). Это означает, что компонент (и связанный с ним атрибут сущности) может некоторое время содержать значение, не удовлетворяющее условиям валидации. Это не является проблемой, так как обычно компоненты ввода с валидацией располагаются в экране редактирования, а он автоматически вызывает валидацию всех своих компонентов перед коммитом. Если же компонент находится не в экране редактирования, то необходимо вызывать его метод validate() в контроллере явно. |
В XML-дескрипторе экрана валидатор для компонента может быть задан во вложенном элементе validator. Возможные атрибуты элемента validator:
Groovy-валидатор и стандартные классы Java-валидаторов, расположенные в пакете com.haulmont.cuba.gui.components.validators поддерживают атрибут message − сообщение, выводимое пользователю в случае ошибки валидации. Атрибут должен содержать сообщение или ключ в пакете сообщений экрана, например:
<validator class="com.haulmont.cuba.gui.components.validators.PatternValidator"
message="msg://validationError"
pattern="\d{3}"/>
# messages.properties
validationError = Input error
Валидатор можно установить с помощью интерфейса CUBA Studio. Пример добавления валидатора к полю fieldGroup:
Выбор механизма валидации осуществляется следующим образом:
<field property="amount">
<validator class="com.haulmont.cuba.gui.components.validators.DoubleValidator"/>
</field>
<field property="year">
<validator>
value ==~ /\d+/
</validator>
</field>
<field property="zipCode">
<validator script="com.company.demo.web.address.ZipValidator"/>
</field>
В выражение или скрипт Groovy будет передана одна переменная value, содержащая значение, введенное в визуальном компоненте. Выражение или скрипт должны вернуть boolean значение: true − valid, false − not valid.
Если в качестве валидатора используется класс Java, то он должен иметь либо дефолтный конструктор без параметров, либо конструктор со следующим набором параметров:
|
Если валидатор реализован внутренним классом, то он должен быть объявлен с модификатором static, а его имя для загрузки отделяется символом "$", например: <validator class="com.sample.sales.gui.AddressEdit$ZipValidator"/> |
Платформа уже содержит несколько реализаций наиболее часто используемых валидаторов (см. пакет com.haulmont.cuba.gui.components.validators), которые можно применять в проектах:
Валидатор-класс можно назначить компоненту не только в XML-дескрипторе экрана, но и программно, передавая экземпляр валидатора в метод addValidator() компонента.
Пример создания класса валидатора почтового индекса:
public class ZipValidator implements Field.Validator {
@Override
public void validate(Object value) throws ValidationException {
if (value != null && ((String) value).length() != 6)
throw new ValidationException("Zip must be of 6 characters length");
}
}
Использование валидатора почтового индекса и стандартного валидатора по шаблону в полях компонента FieldGroup:
<fieldGroup>
<field property="zip" required="true">
<validator class="com.company.sample.gui.ZipValidator"/>
</field>
<field property="imei">
<validator class="com.haulmont.cuba.gui.components.validators.PatternValidator"
pattern="\d{15}"
message="IMEI validation failed"/>
</field>
</fieldGroup>
Пример программного задания валидатора:
if (Boolean.TRUE.equals(parameter.getRequired())) {
tokenList.addValidator(new Field.Validator() {
@Override
public void validate(Object value) throws ValidationException {
if (value instanceof Collection && CollectionUtils.isEmpty((Collection) value)) {
throw new ValidationException(getMessage("paramIsRequiredButEmpty"));
}
}
});
}
Доступно для всех визуальных компонентов
com.vaadin.ui.TextField vTextField = textField.unwrap(com.vaadin.ui.TextField.class);
if (textArea.isModified()) {
textArea.commit();
}
Доступно для компонентов:
CheckBox - CurrencyField - DateField - FileUploadField - LookupField - LookupPickerField - PickerField - TextArea - TextField - TimeField
groupBox.addExpandedStateChangeListener(e ->
showNotification("Expanded: " + groupBox.isExpanded()));
Доступно для компонентов:
Доступно для компонентов:
Accordion - BoxLayout - CssLayout - Frame - GridLayout - GroupBoxLayout - HtmlBoxLayout - ScrollBoxLayout - SplitPanel - TabSheet
Доступно для компонентов:
BoxLayout - CssLayout - Frame - GroupBoxLayout - ScrollBoxLayout -
textArea.setContextHelpIconClickHandler(event ->
showMessageDialog("Title", "Body message",
MessageType.CONFIRMATION_HTML
.modal(false)));
Доступно для компонентов:
CheckBox - ColorPicker - CurrencyField - DateField - DatePicker - FileUploadField - LookupField - LookupPickerField - MaskedField - OptionsGroup - OptionsList - PasswordField - PickerField - RichTextArea - SearchPickerField - SourceCodeEditor - SuggestionPickerField - TextArea - TextField - TimeField - TokenList - TwinColumn
Доступно для компонентов:
DataGrid - Filter - GroupBoxLayout - SplitPanel - Table - TextArea
textField.addValueChangeListener(e ->
showNotification("Before: " + e.getPrevValue() + ". After: " + e.getValue()));
Доступно для компонентов:
CheckBox - ColorPicker - CurrencyField - DateField - DatePicker - FileUploadField - Label - LookupField - LookupPickerField - MaskedField - OptionsGroup - OptionsList - PasswordField - PickerField - ProgressBar - RichTextArea - SearchPickerField - SourceCodeEditor - SuggestionPickerField - TextArea - TextField - TimeField - TokenList - TwinColumn
vbox.addLayoutClickListener(event ->
showNotification("Clicked"));
Доступно для компонентов:
ButtonsPanel - BoxLayout - CssLayout - GridLayout
vbox.setMargin(true);
vbox.setMargin(true, false, true, false);
vbox.setMargin(new MarginInfo(true, false, false, true));
Доступно для компонентов:
BoxLayout - Filter - Frame - GridLayout - ScrollBoxLayout
groupBox.setOuterMargin(true);
groupBox.setOuterMargin(true, false, true, false);
groupBox.setOuterMargin(new MarginInfo(true, false, false, true));
Доступно для компонентов:
cssLayout.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
Доступно для компонентов:
BoxLayout - ButtonsPanel - CssLayout - GridLayout - GroupBoxLayout - ScrollBoxLayout
vbox.setSpacing(true);
Доступно для компонентов:
BoxLayout - ButtonsPanel - Frame - GridLayout GroupBoxLayout - ScrollBoxLayout
5.5.2.5. XML-атрибуты компонентов
Атрибут, задающий расположение компонента относительно вышестоящего контейнера.
Возможные значения:
Атрибут, устанавливающий заголовок для визуального компонента.
Значением атрибута должна быть либо строка сообщения, либо ключ в пакете сообщений. В случае ключа значение должно начинаться с префикса msg://
Способы задания ключа:
caption="msg://infoFieldCaption"
caption="msg://com.company.sample.gui.screen/infoFieldCaption"
Задает имя атрибута сущности, отображаемого компонентом. Используется только для сущностей, находящихся в источнике данных (например заданном для LookupField свойством optionsDatasource).
Если captionProperty не задан, будет отображаться имя экземпляра.
Указывает, сколько колонок сетки должен занять компонент (по умолчанию 1).
Данный атрибут может быть назначен любому компоненту, находящемуся непосредственно внутри контейнера GridLayout.
Атрибут, задающий текст контекстной подсказки для компонента. Если установлено значение, рядом с полем будет отображаться специальный значок ?. Если поле имеет отдельный заголовок, то есть, установлены атрибуты caption или icon, значок подсказки будет отображаться рядом с заголовком, в противном случае - рядом с самим полем:
В web-клиенте подсказка отображается при наведении курсора мыши на значок ?, в клиенте desktop пользователь должен кликнуть на значок ?, чтобы увидеть подсказку.
<textField id="textField"
contextHelpText="msg://contextHelp"/>
Указывает, может ли текст контекстной подсказки быть обработан как HTML.
<textField id="textField"
description="Description"
contextHelpText="<p><h1>Lorem ipsum dolor</h1> sit amet, <b>consectetur</b> adipiscing elit.</p><p>Donec a lobortis nisl.</p>"
contextHelpTextHtmlEnabled="true"/>
Возможные значения − true, false.
Предназначен для задания источника данных, описанного в секции dsContext XML-дескриптора экрана.
При указании атрибута datasource для компонента, реализующего интерфейс DatasourceComponent, необходимо также задать атрибут property.
Атрибут, задающий текст подсказки для компонента, отображаемой при наведении курсора мыши или клике в области компонента.
Атрибут, указывающий на возможность редактирования содержимого компонента (не путать с enable).
Возможные значения − true, false. По умолчанию true.
На возможность редактирования содержимого для компонента, связанного с данными (наследника DatasourceComponent или ListComponent), влияет также подсистема безопасности. Если по данным подсистемы безопасности данный компонент должен быть недоступен для редактирования, значение атрибута editable не принимается во внимание.
Атрибут компонента, устанавливающий его состояние: доступен, недоступен.
Если компонент недоступен, то он не принимает фокус ввода. Недоступность контейнера приводит к тому, что все его компоненты также становятся недоступными. Возможные значения − true, false. По умолчанию все компоненты доступны.
Атрибут контейнера для управления его внутренней компоновкой.
Задает компонент внутри контейнера, который необходимо расширить на все доступное пространство в направлении размещения компонентов. Для контейнера с вертикальным размещением устанавливает компоненту 100% высоту, для контейнера с горизонтальным размещением - 100% ширину. Кроме того, при изменении размера контейнера изменять размер будет именно этот компонент.
Атрибут, устанавливающий высоту компонента.
Может быть задана в пикселях либо в процентах от высоты вышестоящего контейнера. Например: 100px, 100%, 50. Если единица измерения не указана, подразумевается высота в пикселях.
Установка значения в % означает, что компонент по высоте займет соответствующую часть пространства, предоставляемого контейнером более высокого уровня.
При выборе значения AUTO или -1px для компонента устанавливается высота по умолчанию, для контейнера высота определяется по содержимому, то есть суммарной высотой вложенных компонентов.
Атрибут, устанавливающий значок для визуального компонента.
Значением атрибута должен быть путь к файлу значка относительно каталога темы:
icon="icons/create.png"
либо его имя в используемом наборе значков:
icon="CREATE_ACTION"
Если значок должен быть выбран в зависимости от языка пользователя, можно указать путь к нему в пакете сообщений, а в атрибуте icon − ключ сообщения, например:
icon="msg://addIcon"
В веб-клиенте с темой Halo (или производной от нее) вместо файлов можно использовать элементы шрифта Font Awesome. Для этого достаточно указать константу из класса com.vaadin.server.FontAwesome с префиксом font-icon: например:
icon="font-icon:BOOK"
Подробнее об использовании значков можно прочитать в разделе Значки.
Идентификатор компонента.
Рекомендуется формировать значение по правилам Java-идентификаторов и использовать camelСase, например, userGrid, filterPanel.Может быть указан для любого компонента и должен быть уникальным в пределах экрана.
Атрибут inputPrompt задает строку, отображаемую в поле, если его значение равно null.
<suggestionField inputPrompt="Let's search something!"/>
Атрибут используется для компонентов TextField, LookupField, LookupPickerField, SearchPickerField, SuggestionPickerField только в веб-клиенте.
Атрибут margin устанавливает наличие отступа между внешними границами и содержимым контейнера.
Может иметь два вида значений:
По умолчанию отступы отсутствуют.
Идентификатор опции, выбор которой будет равносилен установке значения в null.
Атрибут используется для компонентов LookupField, LookupPickerField, SearchPickerField.
Пример для компонента LookupField, установка значения атрибута в XML-дескрипторе:
<lookupField datasource="orderDs"
property="customer"
nullName="(none)"
optionsDatasource="customersDs" width="200px"/>
Пример для компонента LookupField, установка значения атрибута в контроллере:
<lookupField id="customerLookupField" optionsDatasource="customersDs"
width="200px" datasource="orderDs" property="customer"/>
customerLookupField.setNullOption("<null>");
Задает режим открытия связанного экрана. Соответствует перечислению WindowManager.OpenType со значениями NEW_TAB, THIS_TAB, NEW_WINDOW, DIALOG. По умолчанию THIS_TAB.
Задает имя источника данных, используемого для формирования списка опций.
Совместно с optionsDatasource может использоваться атрибут captionProperty.
Задаёт полное имя класса перечисления, используемого для формирования списка опций.
Атрибут компонента, реализующего интерфейс DatasourceComponent.
Предназначен для задания имени атрибута сущности, значение которого будет отображаться или редактироваться данным визуальным компонентом.
Используется всегда совместно с атрибутом datasource.
Атрибут визуального компонента, реализующего интерфейс Field. Указывает, что в данное поле обязательно должно быть введено значение.
Возможные значения атрибута − true, false. По умолчанию false.
Совместно с required может использоваться атрибут requiredMessage.
XML-атрибут, используемый совместно с атрибутом required. Позволяет установить сообщение, выводимое пользователю в случае нарушения требования required.
Атрибут может содержать сообщение или ключ в пакете сообщений, например: requiredMessage="msg://infoTextField.requiredMessage"
Определяет, должен ли компонент реагировать на изменения размеров доступной области. Реакцию можно задать с помощью стилей.
Возможные значения атрибута − true, false. По умолчанию false.
Указывает, сколько строк сетки должен занять компонент (по умолчанию 1).
Данный атрибут может быть назначен любому компоненту, находящемуся непосредственно внутри контейнера GridLayout.
Определяет, нужно ли сохранять пользовательские настройки отображения компонента. Настройки сохраняются только для компонентов, имеющих id.
Возможные значения атрибута − true, false. По умолчанию true.
Атрибут spacing устанавливает наличие отступов между компонентами внутри контейнера.
Возможные значения − true, false.
По умолчанию отступы отсутствуют.
Атрибут, задающий имя стиля компонента. Подробнее см. Темы приложения.
В теме halo определено несколько стандартных стилей для компонентов:
Определяет, разрешена ли HTML-разметка в заголовках вкладок. Если выбрано значение true, заголовки отображаются в браузере как HTML, при этом ответственность за безопасность используемого HTML-кода несёт сам разработчик. Если выбрано значение false, содержимое заголовка отображается как обычный текст.
Возможные значения − true, false. По умолчанию false.
Определяет, может ли компонент принимать фокус, и задаёт относительный порядок перехода фокуса между компонентами экрана.
Может принимать положительное или отрицательное целочисленное значение:
Определяет, должна ли область выбора вкладок отображаться в UI.
Возможные значения − true, false. По умолчанию true.
Определяет, разрешено ли выделение текста в ячейках таблицы.
Возможные значения атрибута − true, false. По умолчанию false.
Атрибут, устанавливающий видимость компонента. Возможные значения − true, false.
Если контейнер невидим, не видны и все его компоненты. По умолчанию все компоненты видимы.
Атрибут, устанавливающий ширину компонента.
Значение может быть задано в пикселях или в процентах от ширины вышестоящего контейнера. Например: 100px, 100%, 50. Если единица измерения не указана, подразумевается ширина в пикселях. Простановка значения в % означает, что компонент по ширине займет соответствующую часть пространства, предоставляемого контейнером более высокого уровня.
При выборе значения AUTO или -1px для компонента устанавливается ширина по умолчанию, для контейнера ширина определяется по содержимому, то есть суммарной шириной вложенных компонентов.
Источники данных обеспечивают работу связанных с данными (data-aware) компонентов.
Визуальные компоненты сами не обращаются к Middleware, а получают экземпляры сущностей из связанных источников данных. При этом один источник данных может обслуживать несколько визуальных компонентов, если им нужен один и тот же экземпляр или набор экземпляров.
Связь визуального компонента и источника данных проявляется в следующем:
@Inject
private FieldGroup fieldGroup;
@Inject
private Datasource<Order> orderDs;
@Named("fieldGroup.customer")
private PickerField customerField;
public void init(Map<String, Object> params){
Customer customer;
// Get customer from component: not for common use
Component component = fieldGroup.getFieldNN("customer").getComponentNN();
customer = ((HasValue)component).getValue();
// Get customer from component
customer = customerField.getValue();
// Get customer from datasource: recommended
customer = orderDs.getItem().getCustomer();
}
Как видно из примера, работа со значениями атрибутов сущностей через компонент - не самый прямолинейный способ. В первом случае он требует приведения типа и указания поля FieldGroup в виде строки. Второй пример более безопасный и прямой, но он требует знания ещё и типа поля, которое должно быть инжектировано в контроллер. В то же время, получив методом getItem() из источника данных хранящийся в нем экземпляр, можно напрямую читать и изменять значения его атрибутов.
|
Как правило, визуальный компонент привязывается к атрибуту, непосредственно принадлежащему сущности, находящейся в источнике данных. В приведенном выше примере компонент привязан к атрибуту customer сущности Order. Можно также привязать компонент к атрибуту связанной сущности, например к customer.name. В этом случае компонент будет корректно отображать значение атрибута name, но при его изменении пользователем слушатели источника данных вызваны не будут, и изменения не будут сохранены. Поэтому привязывать компонент к атрибутам второго и более порядка имеет смысл только для отображения, например в Label, колонке Table или установив для TextField свойство editable = false. |
Источники данных также отслеживают изменения содержащихся в них сущностей, и могут отправлять измененные экземпляры обратно на Middleware для сохранения в базе данных.
Рассмотрим основные интерфейсы источников.
Рисунок 22. Интерфейсы источников данных
Стандартной реализацией такого источника является класс DatasourceImpl, который используется, например, как главный источник данных в экранах редактирования сущностей.
Способ загрузки коллекции сущностей определяется реализацией. Наиболее типичный - загрузка с Middleware через DataManager, при этом для формирования JPQL запроса используются методы setQuery(), setQueryFilter().
Стандартной реализацией такого источника является класс CollectionDatasourceImpl, который используется в экранах, отображающих списки сущностей.
Стандартной реализацией является класс GroupDatasourceImpl.
Стандартной реализацией является класс HierarchicalDatasourceImpl.
Например, в источнике dsOrder установлен экземпляр сущности Order, содержащий ссылку на экземпляр Customer. Тогда для связи экземпляра Customer с визуальными компонентами достаточно создать NestedDatasource, у которого хозяином является dsOrder, а мета-свойство указывает на атрибут Order.customer.
Стандартные реализации: для работы с одним экземпляром - PropertyDatasourceImpl, для работы с коллекцией - CollectionPropertyDatasourceImpl, GroupPropertyDatasourceImpl, HierarchicalPropertyDatasourceImpl. Последние реализуют также интерфейс CollectionDatasource, однако некоторые его нерелевантные методы, связанные с загрузкой, например, setQuery(), выбрасывают UnsupportedOperationException.
Стандартной реализацией является класс EmbeddedDatasourceImpl.
Как правило, источники данных объявляются декларативно в секции dsContext дескриптора экрана.
Автоматическое обновление CollectionDatasource
При открытии экрана, его визуальные компоненты, соединенные с источниками типа CollectionDatasource, заставляют эти источники загрузить данные. В результате, таблицы отображают данные сразу после открытия экрана, без всякого дополнительного действия пользователя. Если вы хотите предотвратить автоматическую загрузку, то установите параметр экрана DISABLE_AUTO_REFRESH в true в методе init() этого экрана, или передайте его из вызывающего кода. Данный параметр определен в перечислении WindowParams, поэтому он может быть задан следующим образом:
@Override
public void init(Map<String, Object> params) {
WindowParams.DISABLE_AUTO_REFRESH.set(params, true);
}
В этом случае, источники данных типа CollectionDatasource будут загружены только когда будет вызван их метод refresh(). Это может быть сделано как кодом приложения, так и в случае, если пользователь нажмет кнопку Search компонента Filter.
5.5.3.1. Создание источников данных
Объекты источников данных могут быть созданы как декларативно - путем объявления в XML-дескрипторе экрана, так и программно в контроллере. Обычно используются стандартные реализации интерфейсов источников, однако при необходимости можно создать собственный класс, унаследовав его от стандартного.
5.5.3.1.1. Декларативное создание
Как правило, источники данных объявляются декларативно в элементе dsContext дескриптора экрана. В зависимости от взаимного расположения элементов объявлений создаются источники двух разновидностей:
Пример объявления источников данных:
<dsContext>
<datasource id="carDs" class="com.haulmont.sample.entity.Car" view="carEdit">
<collectionDatasource id="allocationsDs" property="driverAllocations"/>
<collectionDatasource id="repairsDs" property="repairs"/>
</datasource>
<collectionDatasource id="colorsDs" class="com.haulmont.sample.entity.Color" view="_local">
<query>
<![CDATA[select c from sample$Color c order by c.name]]>
</query>
</collectionDatasource>
</dsContext>
Здесь источник carDs содержит один экземпляр сущности Car, а вложенные в него allocationsDs и repairsDs содержат коллекции связанных сущностей из атрибутов Car.driverAllocations и Car.repairs соответственно. Экземпляр Car вместе со связанными сущностями проставляется в источник данных извне. Если данный экран является экраном редактирования, то это происходит автоматически при открытии экрана. Источник данных colorsDs содержит коллекцию экземпляров сущности Color, загружаемую самим источником по указанному JPQL-запросу с представлением _local.
Рассмотрим схему XML.
dsContext - корневой элемент.
Элементы dsContext:
Атрибуты:
Атрибуты collectionDatasource:
@Override
public void init(Map<String, Object> params) {
Set<Customer> entities = (Set<Customer>) params.get("customers");
for (Customer entity : entities) {
customersDs.includeItem(entity);
}
customersDs.refresh();
}
Элементы collectionDatasource:
Специфическим атрибутом является hierarchyProperty, задающий имя атрибута сущности, по которому строится иерархия.
Класс реализации источника выбирается неявно на основе имени элемента XML и, как было сказано выше, взаимного расположения элементов. Однако если необходимо применить нестандартный источник данных, его класс может быть явно указан в атрибуте datasourceClass.
5.5.3.1.2. Программное создание
При необходимости создать источник данных в Java коде рекомендуется воспользоваться специальным классом DsBuilder.
Экземпляр DsBuilder параметризуется цепочкой вызовов его методов в стиле текучего (fluent) интерфейса. Если установлены параметры master и property, то в результате будет создан NestedDatasource, в противном случае - Datasource или CollectionDatasource.
Пример:
CollectionDatasource ds = new DsBuilder(getDsContext())
.setJavaClass(Order.class)
.setViewName(View.LOCAL)
.setId("ordersDs")
.buildCollectionDatasource();
5.5.3.1.3. Собственные классы реализации
Как правило, нестандартная реализация источника данных требуется для изменения процесса загрузки коллекции сущностей. При создании класса такого источника рекомендуется унаследовать его от CustomCollectionDatasource, либо от CustomGroupDatasource или CustomHierarchicalDatasource, и определить метод getEntities().
Пример:
public class MyDatasource extends CustomCollectionDatasource<SomeEntity, UUID> {
private SomeService someService = AppBeans.get(SomeService.NAME);
@Override
protected Collection<SomeEntity> getEntities(Map<String, Object> params) {
return someService.getEntities();
}
}
Для создания экземпляра нестандартного источника данных декларативным способом необходимо указать класс в атрибуте datasourceClass элемента XML. При программном создании через DsBuilder класс источника указывается вызовом setDsClass() или в параметре одного из методов build*().
5.5.3.2. Запросы в CollectionDatasourceImpl
Класс CollectionDatasourceImpl и его наследники GroupDatasourceImpl, HierarchicalDatasourceImpl являются стандартной реализацией источников данных, работающих с коллекциями независимых экземпляров сущностей. Эти источники загружают данные через DataManager, отправляя на Middleware запрос на языке JPQL. Далее рассматриваются особенности формирования таких запросов.
5.5.3.2.1. Возвращаемые значения
Запрос должен возвращать сущности того типа, который указан при создании источника данных. Тип сущности при декларативном создании указывается в атрибуте class элемента XML, при создании через DsBuilder - в методе setJavaClass() или setMetaClass().
Например, запрос источника данных типа Customer может выглядеть следующим образом:
select c from sales$Customer c
или
select o.customer from sales$Order o
Запрос не может возвращать агрегированные значения или отдельные атрибуты, например:
select c.id, c.name from sales$Customer c /* неверно - возвращает отдельные поля, а не весь объект Customer */
JPQL-запрос в источнике данных может содержать параметры нескольких видов. Вид параметра определяется по префиксу имени параметра. Префиксом является часть имени до знака "$". Интерпретация имени после "$" рассматривается ниже.
Значением параметра являются данные другого источника данных, зарегистрированного в этом же DsContext. Например:
<collectionDatasource id="customersDs" class="com.sample.sales.entity.Customer" view="_local">
<query>
<![CDATA[select c from sales$Customer c]]>
</query>
</collectionDatasource>
<collectionDatasource id="ordersDs" class="com.sample.sales.entity.Order" view="_local">
<query>
<![CDATA[select o from sales$Order o where o.customer.id = :ds$customersDs]]>
</query>
</collectionDatasource>
В данном случае параметром запроса источника данных ordersDs будет текущий экземпляр сущности, находящийся в источнике данных customersDs.
При использовании параметров с префиксом ds между источниками данных автоматически создаются зависимости, приводящие к обновлению источника если меняется значение его параметра. В приведенном примере если изменяется выбранный Покупатель, автоматически обновляется список его Заказов.
Обратите внимание, что в примере запроса с параметром левой частью оператора сравнения является значение идентификатора o.customer.id, а правой - экземпляр Customer, содержащийся в источнике customersDs. Такое сравнение допустимо, так как при выполнении запроса на Middleware реализация интерфейса Query, присваивая значения параметрам запроса, автоматически подставляет ID сущности вместо переданного экземпляра сущности.
В имени параметра после префикса и имени источника может быть также указан путь по графу сущностей к атрибуту, из которого нужно взять значение, например:
<query>
<![CDATA[select o from sales$Order o where o.customer.id = :ds$customersDs.id]]>
</query>
или
<query>
<![CDATA[select o from sales$Order o where o.tagName = :ds$customersDs.group.tagName]]>
</query>
Значение параметра будет взято из объекта Map<String, Object>, переданного в метод refresh() источника данных. Например:
<collectionDatasource id="ordersDs" class="com.sample.sales.entity.Order" view="_local">
<query>
<![CDATA[select o from sales$Order o where o.number = :custom$number]]>
</query>
</collectionDatasource>
ordersDs.refresh(ParamsMap.of("number", "1"));
Приведение экземпляра при необходимости к его идентификатору осуществляется аналогично параметрам с префиксом ds. Путь по графу сущностей в имени параметра в данном случае не поддерживается.
Значение параметра будет взято из объекта Map<String, Object>, переданного при открытии экрана в метод init() контроллера. Например:
<query>
<![CDATA[select e from sales$Order e where e.customer = :param$customer]]>
</query>
openWindow("sales$Order.lookup", WindowManager.OpenType.DIALOG, ParamsMap.of("customer", customersTable.getSingleSelected()));
Приведение экземпляра при необходимости к его идентификатору осуществляется аналогично параметрам с префиксом ds. Поддерживается путь к атрибуту по графу сущностей в имени параметра.
Значением параметра будет текущее значение визуального компонента, путь к которому указан в имени параметра. Например:
<query>
<![CDATA[select o from sales$Order o where o.number = :component$filter.orderNumberField]]>
</query>
Путь к компоненту должен включать все вложенные фреймы.
Приведение экземпляра при необходимости к его идентификатору аналогично параметрам ds. Поддерживается путь к атрибуту по графу сущностей в имени параметра как продолжение пути к компоненту.
|
При изменении значения компонента источник данных автоматически не обновляется. |
Значением параметра будет значение атрибута пользовательской сессии, указанного в имени параметра.
Значение извлекается методом UserSession.getAttribute(), поэтому поддерживаются также предопределенные имена атрибутов сессии:
Пример:
<query>
<![CDATA[select o from sales$Order o where o.createdBy = :session$userLogin]]>
</query>
Приведение экземпляра при необходимости к его идентификатору аналогично параметрам ds. Путь по графу сущностей в имени параметра в данном случае не поддерживается.
|
Если значение параметра не найдено по правилам, задаваемым префиксом, для данного параметра устанавливается значение null. То есть если, например, в запросе указан параметр с именем param$some_name, а в мэп параметров экрана нет ключа some_name, то для параметра param$some_name устанавливается значение null. |
Запрос источника данных может быть модифицирован во время работы приложения, в зависимости от вводимых пользователем условий, что позволяет эффективно фильтровать данные на уровне выборки из БД.
Простейший способ обеспечения такой возможности - подключение к источнику данных специального визуального компонента Filter.
Если по какой-то причине применение универсального фильтра нежелательно, можно встроить в текст запроса специальную разметку на XML, позволяющую сформировать итоговый запрос в зависимости от значений, введенных пользователем в произвольные визуальные компоненты экрана.
В таком фильтре могут быть использованы следующие элементы:
Условия и предложения добавляются в итоговый запрос, только если присутствующие внутри них параметры получили значения, т.е. не равны null.
|
В фильтрах запросов можно использовать только параметры custom, param, component и session. Параметр ds не будет работать. |
Пример:
<query>
<![CDATA[select distinct d from app$GeneralDoc d]]>
<filter>
<or>
<and>
<c join=", app$DocRole dr">dr.doc.id = d.id and d.processState = :custom$state</c>
<c>d.barCode like :component$barCodeFilterField</c>
</and>
<c join=", app$DocRole dr">dr.doc.id = d.id and dr.user.id = :custom$initiator</c>
</or>
</filter>
</query>
В данном случае если в метод refresh() источника данных переданы параметры state и initiator, а в визуальном компоненте barCodeFilterField установлено некоторое значение, то итоговый запрос примет вид:
select distinct d from app$GeneralDoc d, app$DocRole dr
where
(
(dr.doc.id = d.id and d.processState = :custom$state)
and
(d.barCode like :component$barCodeFilterField)
)
or
(dr.doc.id = d.id and dr.user.id = :custom$initiator)
Если же, к примеру, компонент barCodeFilterField пуст, а в refresh() передан только параметр initiator, то запрос получится следующим:
select distinct d from app$GeneralDoc d, app$DocRole dr
where
(dr.doc.id = d.id and dr.user.id = :custom$initiator)
5.5.3.2.4. Поиск подстроки без учета регистра
В источниках данных можно использовать особенность выполнения JPQL-запросов, описанную для интерфейса Query уровня Middleware: для удобного формирования условия поиска без учета регистра символов и по любой части строки можно использовать префикс (?i). Однако, в связи с тем, что значение параметра обычно передается неявно, имеются следующие отличия:
Для примера рассмотрим обработку следующего запроса:
select c from sales$Customer c where c.name like :(?i)component$customerNameField
В данном случае значение параметра, взятое из компонента customerNameField, будет переведено в нижний регистр и обрамлено символами %, а затем в базе данных будет выполнен SQL запрос с условием вида lower(C.NAME) like ?
Следует иметь в виду, что при таком поиске индекс, созданный в БД по полю NAME, не используется.
Value datasources позволяют выполнять запросы, возвращающие скалярные и агрегатные значения. Например, следующим запросом можно загрузить некоторую агрегатную статистику по покупателям:
select o.customer, sum(o.amount) from demo$Order o group by o.customer
Value datasources работают с сущностями особого типа: KeyValueEntity. Такая сущность может содержать произвольный набор атрибутов, задаваемый во время работы приложения. Так, в примере выше, экземпляры KeyValueEntity будут содержать два атрибута: первый типа Customer, второй типа BigDecimal.
Реализации value datasources унаследованы от других широко используемых классов источников данных и дополнительно реализуют специфический интерфейс ValueDatasource. На диаграмме ниже изображены реализации value datasources и их базовые классы:
Интерфейс ValueDatasource содержит следующие методы:
Value datasources могут быть заданы декларативно в XML-дескрипторе. Существует три XML-элемента, соответствующих классам реализации:
XML описание value datasource должно содержать элемент properties, который задает атрибуты KeyValueEntity, содержащихся в источнике данных (см. метод addProperty() выше). Порядок элементов property должен соответствовать порядку колонок в результирующем наборе, возвращаемом запросом. Например, в следующем определении атрибут customer получит значение из колонки o.customer, а атрибут sum из колонки sum(o.amount):
<dsContext>
<valueCollectionDatasource id="salesDs">
<query>
<![CDATA[select o.customer, sum(o.amount) from demo$Order o group by o.customer]]>
</query>
<properties>
<property class="com.company.demo.entity.Customer" name="customer"/>
<property datatype="decimal" name="sum"/>
</properties>
</valueCollectionDatasource>
</dsContext>
Value datasources предназначены только для чтения данных, так как сущность KeyValueEntity является неперсистентной и не может быть сохранена стандартным механизмом работы с БД.
Value datasource можно создать как вручную, так и с помощью Studio на вкладке Datasources дизайнера экранов.
Окно редактирования атрибутов Properties позволяет создать атрибуты источника данных с указанным типом данных и/или Java-класса.
5.5.3.4. Слушатели источников данных
Слушатели источников данных (datasource listeners) позволяют получать оповещения об изменении состояния источников данных и экземпляров сущностей, в них находящихся.
Существует четыре типа слушателей. Три из них: ItemPropertyChangeListener, ItemChangeListener и StateChangeListener определены в интерфейсе Datasource и могут быть использованы в любых источниках данных. CollectionChangeListener определен в интерфейсе CollectionDatasource и может быть использован только в источниках данных, работающих с коллекциями сущностей.
По сравнению с ValueChangeListener, слушатели источников данных удобнее для использования с точки зрения контроля над жизненным циклом экрана, поэтому рекомендуется использовать их с компонентами, привязанными к источникам данных.
Пример использования слушателей источников данных:
public class EmployeeBrowse extends AbstractLookup {
private Logger log = LoggerFactory.getLogger(EmployeeBrowse.class);
@Inject
private CollectionDatasource<Employee, UUID> employeesDs;
@Override
public void init(Map<String, Object> params) {
employeesDs.addItemPropertyChangeListener(event -> {
log.info("Property {} of {} has been changed from {} to {}",
event.getProperty(), event.getItem(), event.getPrevValue(), event.getValue());
});
employeesDs.addStateChangeListener(event -> {
log.info("State of {} has been changed from {} to {}",
event.getDs(), event.getPrevState(), event.getState());
});
employeesDs.addItemChangeListener(event -> {
log.info("Datasource {} item has been changed from {} to {}",
event.getDs(), event.getPrevItem(), event.getItem());
});
employeesDs.addCollectionChangeListener(event -> {
log.info("Datasource {} content has been changed due to {}",
event.getDs(), event.getOperation());
});
}
}
Интерфейсы слушателей описаны ниже.
Слушатель ItemPropertyChangeListener можно использовать для действий в ответ на изменение пользователем сущности из UI, то есть редактирования полей ввода.
Для Datasource это происходит при установке другого экземпляра (или null) методом setItem().
Для CollectionDatasource данный слушатель вызывается, когда в связанном визуальном компоненте меняется выделенный элемент. Например, это может быть выделенная строка таблицы, элемент дерева, или выделенный элемент выпадающего списка.
Получение оповещения об изменении состояния источника может быть актуально для сложных редакторов, состоящих из нескольких фреймов, где сложно отследить момент проставления редактируемой сущности в источник. В этом случае можно использовать слушатель StateChangeListener для отложенной инициализации некоторых элементов экрана:
employeesDs.addStateChangeListener(event -> {
if (event.getState() == Datasource.State.VALID)
initDataTypeColumn();
});
Все созданные декларативно источники данных регистрируются в объекте DsContext экрана. Ссылку на DsContext можно получить методом getDsContext() контроллера экрана, либо инжекцией в поле класса.
DsContext решает следующие задачи:
Зависимости между источниками организуются с помощью параметров запросов с префиксом ds$.
В качестве примера предположим, что некоторый экран позволяет редактировать экземпляр сущности Order и коллекцию принадлежащих ему экземпляров OrderLine. Экземпляр Order находится в Datasource, коллекция OrderLine - во вложенном CollectionDatasource, созданном по атрибуту Order.lines. Допустим, пользователь изменил какой-то атрибут Order и создал новый экземпляр OrderLine. Тогда при коммите экрана в DataManager будут одновременно отправлены два экземпляра - измененный Order и новый OrderLine. Далее, они вместе попадут в один персистентный контекст и при коммите транзакции сохранятся в БД. Это позволяет не использовать параметров каскадности на уровне ORM и избежать проблем, упомянутых в описании аннотации @OneToMany.
В результате коммита DsContext получает от Middleware набор сохраненных экземпляров (в случае оптимистической блокировки у них, как минимум, увеличено значение атрибута version), и устанавливает эти экземпляры в источниках данных взамен устаревших. Это позволяет сразу после коммита работать со свежими экземплярами без необходимости лишнего обновления источников данных, связанного с запросами к Middleware и базе данных.
Данный механизм необходим, если некоторые сущности, с которыми работает экран, находятся не под управлением источников данных, а создаются и изменяются непосредственно в коде контроллера. Например, визуальный компонент FileUploadField после загрузки файла создает новый экземпляр сущности FileDescriptor, который можно сохранить вместе с другими сущностями экрана именно таким способом - добавив в CommitContext в слушателе BeforeCommitListener.
В следующем примере новый экземпляр Customer будет отправлен на Middleware и сохранен в БД вместе с остальными измененными сущностями экрана при его коммите:
protected Customer customer;
protected void createNewCustomer() {
customer = metadata.create(Customer.class);
customer.setName("John Doe");
}
@Override
public void init(Map<String, Object> params) {
getDsContext().addBeforeCommitListener(context -> {
if (customer != null)
context.getCommitInstances().add(customer);
}
}
DataSupplier - интерфейс, через который источники данных обращаются к Middleware для загрузки и сохранения сущностей. Его стандартная реализация просто делегирует выполнение DataManager. Экран может задать свою реализацию интерфейса DataSupplier в атрибуте dataSupplier элемента window.
Ссылку на DataSupplier можно получить либо инжекцией в контроллер экрана, либо через экземпляры DsContext или Datasource. В обоих случаях возвращается или стандартная, или собственная реализация интерфейса (если таковая определена).
5.5.4. Действия. Интерфейс Action
Action − интерфейс, абстрагирующий действие (другими словами, некоторую функцию) от визуального компонента. Он особенно полезен в случаях, когда одно и то же действие может быть вызвано из разных визуальных компонентов. Кроме того, данный интерфейс позволяет снабдить действие дополнительными свойствами, такими как название, признаки доступности и видимости, и другими.
Рассмотрим методы интерфейса Action:
Для реализации действий рекомендуется использовать декларативное создание действий, либо наследоваться от класса AbstractAction. Кроме того, существует набор стандартных действий, применимых для работы с таблицами и компонентами выбора. От стандартных действий также можно наследоваться для модификации их поведения или перехвата событий.
Визуальные компоненты, связанные с действием, могут быть двух типов:
Связь компонента с действием осуществляется путем вызова метода ActionOwner.setAction() компонента. В этот момент компонент заменяет свои свойства на соответствующие свойства действия (подробнее см. описание компонентов).
Действия добавляются компоненту вызовом метода ActionsHolder.addAction(). Реализация этого метода в компоненте проверяет, нет ли уже в нем действия с таким же идентификатором. Если есть, то имеющееся действие будет заменено на новое переданное. Поэтому можно, например, декларировать стандартное действие в дескрипторе экрана, а затем в контроллере создать новое с переопределенными методами и добавить компоненту.
5.5.4.1. Декларативное создание действий
В XML-дескрипторе экрана для любого компонента, реализующего интерфейс Component.ActionsHolder, в том числе для всего экрана или фрейма, может быть задан набор действий. Делается это в элементе actions, который содержит вложенные элементы action.
Элемент action может иметь следующие атрибуты:
В теме hover подсветка доступна по умолчанию; для её активации в теме halo установите значение true для переменной стиля $cuba-highlight-primary-action.
Следующие действия являются primary по умолчанию, если не установлено иное: CreateAction and SelectAction.
Комбинации можно жёстко задавать в XML-дескрипторе. Возможные модификаторы - ALT, CTRL, SHIFT - отделяются символом "-". Например:
<action id="create" shortcut="ALT-N"/>
Для большей гибкости можно использовать готовые псевдонимы комбинаций из списка ниже, к примеру:
<action id="edit" shortcut="${TABLE_EDIT_SHORTCUT}"/>
Кроме того, есть возможность задавать комбинацию с помощью полного имени интерфейса Config и имени метода, возвращающего нужную комбинацию:
<action id="remove" shortcut="${com.haulmont.cuba.client.ClientConfig#getTableRemoveShortcut}"/>
Рассмотрим примеры декларативного объявления действий.
<window ...>
<dsContext/>
<actions>
<action id="sayHelloAction" caption="msg://sayHello" shortcut="ALT-T" invoke="sayHello"/>
</actions>
<layout>
<button action="sayHelloAction"/>
</layout>
</window>
// controller
public void sayHello(Component component) {
showNotification("Hello!", NotificationType.TRAY);
}
Здесь объявляется действие с идентификатором sayHelloAction и названием из пакета сообщений. С этим действием связывается кнопка, заголовок которой будет установлен в название действия. Действие вызовет метод sayHello() контроллера при нажатии на кнопку, а также при нажатии комбинации клавиш ALT-T, если в данный момент экран принимает фокус ввода.
<popupButton caption="Say something">
<actions>
<action id="helloAction" caption="Say hello" invoke="sayHello"/>
<action id="goodbyeAction" caption="Say goodbye" invoke="sayGoodbye"/>
</actions>
</popupButton>
<table id="usersTable" width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="copy" caption="msg://copy" icon="COPY"
invoke="copy" trackSelection="true"/>
<action id="changePassw" caption="msg://changePassw" icon="EDIT"
invoke="changePassword" trackSelection="true"/>
</actions>
<buttonsPanel>
<button action="usersTable.create"/>
<button action="usersTable.edit"/>
<button action="usersTable.copy"/>
<button action="usersTable.changePassw"/>
</buttonsPanel>
<rowsCount/>
<columns>
<column id="login"/>
...
</columns>
<rows datasource="usersDs"/>
</table>
Здесь помимо стандартных действий таблицы create и edit объявлены действия copy и changePassw, вызывающие соответствующие методы контроллера. Для этих действий указан также атрибут trackSelection="true", в результате чего действие и связанная с ним кнопка становятся недоступными, если в таблице не выбрана ни одна строка. Это удобно, если действие предназначено для выполнения над текущей выбранной строкой таблицы.
Для действий create и edit можно указать дополнительный атрибут openType для указания режима открытия экрана редактирования, как описано для метода setOpenType() класса CreateAction.
<pickerField id="colourField" datasource="carDs" property="colour"/>
<actions>
<action id="lookup"/>
<action id="show" icon="PICKERFIELD_LOOKUP"
invoke="showColour" caption="" description="Show colour"/>
</actions>
</pickerField>
В данном примере для компонента PickerField объявлено стандартное действие lookup и действие show, вызывающее метод showColour() контроллера. Так как в кнопках PickerField, отображающих действия, используются значки, а не надписи, атрибут caption явно установлен в пустую строку, иначе названием действия и заголовком кнопки стал бы идентификатор действия. Атрибут description позволяет отображать всплывающую подсказку при наведении мыши на кнопку действия.
Ссылки на любые декларативно объявленные действия можно получить в контроллере экрана либо непосредственно путем инжекции, либо из компонентов, реализующих интерфейс Component.ActionsHolder. Это может понадобиться для программной установки свойств действия. Например:
@Named("carsTable.create")
private CreateAction createAction;
@Named("carsTable.copy")
private Action copyAction;
@Inject
private PickerField colourField;
@Override
public void init(Map<String, Object> params) {
Map<String, Object> values = new HashMap<>();
values.put("type", CarType.PASSENGER);
createAction.setInitialValues(values);
copyAction.setEnabled(false);
Action showAction = colourField.getAction("show");
showAction.setEnabled(false);
}
Стандартные действия - это классы, имплементирующие интерфейс Action, и предназначенные для решения типовых задач, таких как вызов экрана редактирования для сущности, выбранной в таблице. Стандартные действия имеют строго определенные идентификаторы, поэтому для декларативного объявления некоторого стандартного действия достаточно указать его идентификатор.
Существует два вида стандартных действий:
5.5.4.2.1. Стандартные действия с коллекцией
Для наследников ListComponent (это Table, GroupTable, TreeTable и Tree) набор стандартных действий определен в перечислении ListActionType, классы их реализации находятся в пакете com.haulmont.cuba.gui.components.actions.
Пример использования стандартных действий в таблице:
<table id="usersTable" width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
<action id="refresh"/>
</actions>
<buttonsPanel>
<button action="usersTable.create"/>
<button action="usersTable.edit"/>
<button action="usersTable.remove"/>
<button action="usersTable.refresh"/>
</buttonsPanel>
<rowsCount/>
<columns>
<column id="login"/>
...
</columns>
<rows datasource="usersDs"/>
</table>
Рассмотрим их подробнее.
CreateAction - действие с идентификатором create. Предназначено для создания нового экземпляра сущности и открытия экрана редактирования для этого экземпляра. Если экран редактирования успешно закоммитил новый экземпляр в базу данных, то CreateAction добавляет этот новый экземпляр в источник данных таблицы и делает его выбранным.
В классе CreateAction определены следующие специфические методы:
Так как довольно часто требуется открывать экраны редактирования в другом режиме (как правило, DIALOG), при декларативном создании действия create в элементе action можно указать атрибут openType с нужным значением. Это избавляет от необходимости получать ссылку на действие в контроллере и программно устанавливать данное свойство. Например:
<table id="usersTable">
<actions>
<action id="create" openType="DIALOG"/>
createAction.setWindowParamsSupplier(() -> {
Customer customer = metadata.create(Customer.class);
customer.setCategory(/* some value dependent on the current state of the screen */);
return ParamsMap.of("customer", customer);
});
Map<String, Object> values = new HashMap<>();
values.put("type", CarType.PASSENGER);
carCreateAction.setInitialValues(values);
Пример использования setInitialValues() приведен также в разделе рецептов разработки.
carCreateAction.setInitialValuesSupplier(() ->
ParamsMap.of("type", /* value depends on the current state of the screen */));
customersTableCreate.setBeforeActionPerformedHandler(() -> {
showNotification("The new customer instance will be created");
return isValid();
});
@Named("customersTable.create")
private CreateAction customersTableCreate;
@Override
public void init(Map<String, Object> params) {
customersTableCreate.setAfterCommitHandler(new CreateAction.AfterCommitHandler() {
@Override
public void handle(Entity entity) {
showNotification("Committed", NotificationType.HUMANIZED);
}
});
}
EditAction - действие с идентификатором edit. Открывает экран редактирования для выбранного экземпляра сущности. Если экран редактирования успешно закоммитил экземпляр в базу данных, то EditAction обновляет этот экземпляр в источнике данных таблицы.
В классе EditAction определены следующие специфические методы:
Так как довольно часто требуется открывать экраны редактирования в другом режиме (как правило, DIALOG), при декларативном создании действия edit в элементе action можно указать атрибут openType с нужным значением. Это избавляет от необходимости получать ссылку на действие в контроллере и программно устанавливать данное свойство. Например:
<table id="usersTable">
<actions>
<action id="edit" openType="DIALOG"/>
customersTableEdit.setWindowParamsSupplier(() ->
ParamsMap.of("category", /* some value dependent on the current state of the screen */));
customersTableEdit.setBeforeActionPerformedHandler(() -> {
showNotification("The customer instance will be edited");
return isValid();
});
@Named("customersTable.edit")
private EditAction customersTableEdit;
@Override
public void init(Map<String, Object> params) {
customersTableEdit.setAfterCommitHandler(new EditAction.AfterCommitHandler() {
@Override
public void handle(Entity entity) {
showNotification("Committed", NotificationType.HUMANIZED);
}
});
}
Возвращаемый экземпляр BulkEditorIntegration можно изменить с помощью следующих методов:
@Named("clientsTable.edit")
private EditAction clientsTableEdit;
@Override
public void init(Map<String, Object> params) {
super.init(params);
clientsTableEdit.getBulkEditorIntegration()
.setEnabled(true)
.setOpenType(WindowManager.OpenType.DIALOG);
}
RemoveAction - действие с идентификатором remove. Предназначено для удаления выбранного экземпляра сущности.
В классе RemoveAction определены следующие специфические методы:
Значение autocommit не влияет на работу источников данных в режиме Datasource.CommitMode.PARENT, то есть тех, которые обеспечивают редактирование композиционных сущностей.
customersTableRemove.setBeforeActionPerformedHandler(() -> {
showNotification("The customer instance will be removed");
return isValid();
});
@Named("customersTable.remove")
private RemoveAction customersTableRemove;
@Override
public void init(Map<String, Object> params) {
customersTableRemove.setAfterRemoveHandler(new RemoveAction.AfterRemoveHandler() {
@Override
public void handle(Set removedItems) {
showNotification("Removed", NotificationType.HUMANIZED);
}
});
}
RefreshAction - действие с идентификатором refresh. Предназначено для обновления (перезагрузки) коллекции сущностей. При срабатывании вызывает метод refresh() источника данных, с которым связан компонент.
В классе RefreshAction определены следующие специфические методы:
customersTableRefresh.setRefreshParamsSupplier(() ->
ParamsMap.of("number", /* some value dependent on the current state of the screen */));
AddAction - действие с идентификатором add. Предназначено для выбора существующего экземпляра сущности и добавления его в коллекцию. При срабатывании открывает экран выбора сущностей.
В классе AddAction определены следующие специфические методы:
Так как довольно часто требуется открывать экраны выбора в другом режиме (как правило, DIALOG), при декларативном создании действия add в элементе action можно указать атрибут openType с нужным значением. Это избавляет от необходимости получать ссылку на действие в контроллере и программно устанавливать данное свойство. Например:
<table id="usersTable">
<actions>
<action id="add" openType="DIALOG"/>
tableAdd.setWindowParamsSupplier(() ->
ParamsMap.of("customer", getItem()));
customersTableAdd.setBeforeActionPerformedHandler(() -> {
showNotification("The new customer will be added");
return isValid();
});
ExcludeAction - действие с идентификатором exclude. Позволяет исключать экземпляры сущности из коллекции, не удаляя их из базы данных. Класс данного действия является наследником RemoveAction, однако при срабатывании вызывает у CollectionDatasource не removeItem(), а excludeItem(). Кроме того, для вложенных источников данных ExcludeAction разрывает связь с родительской сущностью, поэтому с помощью данного действия можно организовать редактирование ассоциации one-to-many.
В классе ExcludeAction в дополнение к RemoveAction определены следующие специфические методы:
customersTableExclude.setBeforeActionPerformedHandler(() -> {
showNotification("The selected customer will be excluded");
return isValid();
});
ExcelAction - действие с идентификатором excel. Предназначено для экспорта данных таблицы в формат XLS и выгрузки соответствующего файла. Данное действие можно связать только с компонентами Table, GroupTable и TreeTable.
При программном создании действия можно задать параметр конструктора display, передав реализацию интерфейса ExportDisplay для выгрузки файла. По умолчанию используется стандартная реализация.
В классе ExcelAction определены следующие специфические методы:
customersTableExcel.setBeforeActionPerformedHandler(() -> {
showNotification("The selected data will ve downloaded as an XLS file");
return isValid();
});
5.5.4.2.2. Стандартные действия поля выбора
Для компонентов PickerField, LookupPickerField и SearchPickerField набор стандартных действий определен в перечислении PickerField.ActionType. Реализации являются внутренними классами интерфейса PickerField.
Пример использования стандартных действий в компоненте выбора:
<searchPickerField optionsDatasource="coloursDs"
datasource="carDs" property="colour">
<actions>
<action id="clear"/>
<action id="lookup"/>
<action id="open"/>
</actions>
</searchPickerField>
LookupAction - действие с идентификатором lookup. Предназначено для выбора экземпляра сущности и установки его в качестве значения компонента. При срабатывании открывает экран выбора сущностей.
В классе LookupAction определены следующие специфические методы:
ClearAction - действие с идентификатором clear. Предназначено для очистки (то есть установки в null) текущего значения компонента.
OpenAction - действие с идентификатором open. Предназначено для открытия экрана редактирования экземпляра сущности, являющегося текущим значением компонента.
В классе OpenAction определены следующие специфические методы:
BaseAction - базовый класс реализации действий. От него рекомендуется наследовать собственные нестандартные действия, если возможностей декларативного создания действий не хватает.
При создании конкретного класса действия необходимо определить метод actionPerform() и передать в конструктор BaseAction идентификатор действия. Можно также переопределить любые методы получения свойств действия: getCaption(), getDescription(), getIcon(), getShortcut(), isEnabled(), isVisible(), isPrimary(). Стандартные реализации этих методов возвращают значения, установленные соответствующими set-методами. Исключение составляет метод getCaption(): если название действия явно не установлено методом setCaption(), то он обращается в пакет локализованных сообщений с именем, соответствующим пакету класса действия, и возвращает сообщение с ключом, равным идентификатору действия. Если сообщения с таким ключом нет, то возвращается сам ключ, то есть идентификатор действия.
В качестве альтернативы переопределению методов можно использовать fluent interface для установки свойств и lambda expression для предоставления кода обработки действия: см. методы withXYZ().
BaseAction может изменять свои свойства enabled и visible в соответствии с разрешениями пользователя и текущим контекстом.
BaseAction видим (visible), если:
Действие разрешено (enabled), если:
Примеры использования:
@Inject
private Button helloBtn;
@Override
public void init(Map<String, Object> params) {
helloBtn.setAction(new BaseAction("hello") {
@Override
public boolean isPrimary() {
return true;
}
@Override
public void actionPerform(Component component) {
showNotification("Hello!", NotificationType.TRAY);
}
});
// OR
helloBtn.setAction(new BaseAction("hello")
.withPrimary(true)
.withHandler(e -> showNotification("Hello", NotificationType.TRAY)));
}
В данном случае кнопка helloBtn получит в качестве заголовка строку, находящуюся в пакете сообщений с ключом hello. Для того, чтобы получить название кнопки каким-либо иным способом, можно переопределить метод getCaption() действия.
@Inject
private ComponentsFactory componentsFactory;
@Inject
private BoxLayout box;
@Override
public void init(Map<String, Object>params) {
PickerField pickerField = componentsFactory.createComponent(PickerField.NAME);
pickerField.addAction(new BaseAction("hello") {
@Override
public String getCaption() {
return null;
}
@Override
public String getDescription() {
return getMessage("helloDescription");
}
@Override
public String getIcon() {
return"icons/hello.png";
}
@Override
public void actionPerform(Component component) {
showNotification("Hello!", NotificationType.TRAY);
}
});
// OR
pickerField.addAction(new BaseAction("hello")
.withCaption(null)
.withDescription(getMessage("helloDescription"))
.withIcon("icons/ok.png")
.withHandler(e -> showNotification("Hello", NotificationType.TRAY)));
box.add(pickerField);
}
Здесь анонимный класс-наследник BaseAction используется для задания действия кнопки поля выбора. Заголовок кнопки не отображается, вместо него используется значок и описание, всплывающее при наведении курсора мыши.
@Inject
private Table table;
@Inject
private Security security;
@Override
public void init(Map<String, Object> params) {
table.addAction(new HelloAction());
}
private class HelloAction extends BaseAction {
public HelloAction() {
super("hello");
}
@Override
public void actionPerform(Component component) {
showNotification("Hello " + table.getSingleSelected(), NotificationType.TRAY);
}
@Override
protected boolean isPermitted() {
return security.isSpecificPermitted("myapp.allow-greeting");
}
@Override
public boolean isApplicable() {
return target != null && target.getSelected().size() == 1;
}
}
Здесь объявлен класс HelloAction, экземпляр которого добавляется в список действий таблицы. Действие разрешено пользователям, имеющим специфическое разрешение myapp.allow-greeting, и только когда выбрана одна строка таблицы. Последнее условие реализуется с помощью свойства target действия, которое автоматически устанавливается когда действие добавляется в ListComponent (Table или Tree).
@Inject
private Table table;
@Override
public void init(Map<String, Object> params) {
table.addAction(new ItemTrackingAction("hello") {
@Override
public void actionPerform(Component component) {
showNotification("Hello " + table.getSelected().iterator().next(), NotificationType.TRAY);
}
});
}
5.5.5. Диалоговые окна и уведомления
Для вывода сообщений пользователю можно использовать диалоговые окна и уведомления.
Диалоговые окна имеют заголовок с кнопкой закрытия и отображаются всегда в центре главного окна приложения. Уведомления могут отображаться как в центре, так и в углу приложения, и автоматически исчезать.
Диалоги общего назначения
Диалоговые окна общего назначения вызываются методами showMessageDialog() и showOptionDialog() интерфейса Frame. Этот интерфейс реализуется контроллером экрана, поэтому данные методы можно вызывать напрямую в коде контроллера.
Различие типов сообщений отражается только в пользовательском интерфейсе десктоп-приложений.
Типы сообщений могут быть установлены с параметрами:
Пример вызова диалога:
showMessageDialog("Warning", "Something is wrong", MessageType.WARNING.modal(true).closeOnClickOutside(true));
В качестве кнопок со стандартными названиями и значками удобно использовать анонимные классы, унаследованные от DialogAction. Поддерживаются пять видов действий, определяемых перечислением DialogAction.Type: OK, CANCEL, YES, NO, CLOSE. Названия соответствующих кнопок извлекаются из главного пакета локализованных сообщений.
Пример вызова диалога с кнопками Да и Нет и с заголовком и сообщением, взятыми из пакета локализованных сообщений текущего экрана:
showOptionDialog(
getMessage("confirmCopy.title"),
getMessage("confirmCopy.msg"),
MessageType.CONFIRMATION,
new Action[] {
new DialogAction(DialogAction.Type.YES, Status.PRIMARY).withHandler(e -> copySettings()),
new DialogAction(DialogAction.Type.NO, Status.NORMAL)
}
);
Параметр Status конструктора DialogAction используется для определения визуального стиля кнопки, к которой привязано данное действие. Статус Status.PRIMARY подсвечивает кнопку и задаёт ей выделение по умолчанию. Параметр Status можно не использовать, в этом случае используется подсветка кнопок по умолчанию. Если в showOptionDialog передано несколько действий с Status.PRIMARY, то фокус и стиль получает только кнопка первого такого действия в списке.
Диалоговое окно FileUploadDialog предоставляет базовую функциональность загрузки файлов в промежуточное хранилище. Оно содержит drop zone для перетаскивания файлов извне браузера и кнопку загрузки файла.
Открыть диалог можно с помощью метода openWindow(), в случае успешной загрузки окно будет закрыто с COMMIT_ACTION_ID. Закрытие диалога можно отслеживать с помощью слушателей CloseListener и CloseWithCommitListener. Чтобы получить UUID и имя загруженного файла, используйте методы getFileId() и getFileName(). Затем для файла можно, например, создать FileDescriptor, позволяющий ссылаться на него из объектов модели данных, или реализовать другую логику.
FileUploadDialog dialog = (FileUploadDialog) openWindow("fileUploadDialog", OpenType.DIALOG);
dialog.addCloseWithCommitListener(() -> {
UUID fileId = dialog.getFileId();
String fileName = dialog.getFileName();
FileDescriptor fileDescriptor = fileUploadingAPI.getFileDescriptor(fileId, fileName);
// your logic here
});
Уведомления вызываются методом showNotification() интерфейса Frame. Этот интерфейс реализуется контроллером экрана, поэтому данный метод можно вызывать напрямую в коде контроллера.
Метод showNotification() принимает следующие параметры:
Примеры вызова уведомлений:
showNotification(getMessage("selectBook.text"), NotificationType.HUMANIZED);
showNotification("Validation error", "<b>Date</b> is incorrect", NotificationType.TRAY_HTML);
Механизм фоновых задач предназначен для асинхронного выполнения длительных операций на клиентском уровне без заморозки пользовательского интерфейса.
Использование фоновых задач:
Если экран указан, то при его закрытии пользователем активная задача будет прервана. Кроме того, задача будет автоматически прервана по истечении указанного таймаута.
Собственно действия, выполняемые задачей, реализуются в методе run().
|
Метод run() класса BackgroundTask нельзя использовать для чтения/изменения состояния визуальных компонентов или источников данных: вместо этого используйте методы done(), progress() и canceled(). При попытке установить значение для компонента UI из фонового потока будет выброшено исключение IllegalConcurrentAccessException. |
Пример:
@Inject
protected BackgroundWorker backgroundWorker;
@Override
public void init(Map<String, Object> params) {
// Create task with 10 sec timeout and this screen as owner
BackgroundTask<Integer, Void> task = new BackgroundTask<Integer, Void>(10, this) {
@Override
public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
// Do something in background thread
for (int i = 0; i < 5; i++) {
TimeUnit.SECONDS.sleep(1); // time consuming computations
taskLifeCycle.publish(i); // publish current progress to show it in progress() method
}
return null;
}
@Override
public void canceled() {
// Do something in UI thread if the task is canceled
}
@Override
public void done(Void result) {
// Do something in UI thread when the task is done
}
@Override
public void progress(List<Integer> changes) {
// Show current progress in UI thread
}
};
// Get task handler object and run the task
BackgroundTaskHandler taskHandler = backgroundWorker.handle(task);
taskHandler.execute();
}
Подробная информация о назначении методов приведена в JavaDocs классов BackgroundTask, TaskLifeCycle, BackgroundTaskHandler.
Ниже приведены моменты, на которые следует обратить внимание:
public String run(TaskLifeCycle<Integer> taskLifeCycle) {
for (int i = 0; i < 9_000_000; i++) {
if (taskLifeCycle.isCancelled()) {
log.info(" >>> Task was cancelled");
break;
} else {
log.info(" >>> Task is working: iteration #" + i);
}
}
return "Done";
}
|
В блоке Web Client фоновые задачи используют технологию HTTP push, предоставляемую фреймворком Vaadin. См. https://vaadin.com/wiki/-/wiki/Main/Working+around+push+issues для получения информации о настройке веб-серверов для использования данной технологии. |
|
Если вы не используете фоновую задачу, но хотите изменять состояние UI-компонентов из не-UI потока, воспользуйтесь методами интерфейса UIAccessor. Получите ссылку на интерфейс UIAccessor методом BackgroundWorker.getUIAccessor() в UI-потоке, и после этого вы сможете вызывать его методы access() и accessSynchronously() из фонового потока для безопасного чтения и изменения состояния UI-компонентов. |
Тема служит для управления визуальным представлением приложения.
5.5.7.1. Тема в веб-приложениях
Тема веб-приложения состоит из файлов SCSS и других ресурсов, в том числе файлов изображений.
Платформа предоставляет несколько стандартных тем, которые "из коробки" доступны для использования в проекте. Расширение темы позволяет модифицировать существующую тему на уровне проекта. Вы также можете создавать свои собственные темы, которые будут доступны наряду со стандартными.
Если требуется использовать тему в нескольких проектах, ее можно включить в компонент приложения, или создать JAR с темой для повторного использования.
5.5.7.1.1. Использование существующих тем
Платформа включает в себя три готовые темы: Hover, Halo и Havana. Приложение будет по умолчанию использовать ту из них, которая указана в свойстве приложения cuba.web.theme.
Пользователь может выбрать другую доступную тему в стандартном экране Help → Settings. Если вы не хотите, чтобы пользователи имели возможность сами выбирать тему, зарегистрируйте экран settings в файле web-screens.xml проекта с параметром changeThemeEnabled = false:
<screen id="settings" template="/com/haulmont/cuba/web/app/ui/core/settings/settings-window.xml">
<param name="changeThemeEnabled" value="false"/>
</screen>
5.5.7.1.2. Расширение существующей темы
Существующая в платформе тема может быть изменена в проекте приложения. В измененной теме можно сделать следующее:
Структура темы и скрипты сборки
Тема описывается в файлах SCSS. Для изменения (расширения) темы в проекте необходимо создать специальную файловую структуру в модуле web.
Это удобно сделать с помощью CUBA Studio: откройте секцию Project properties и нажмите ссылку Create theme extension. В диалоговом окне выберите тему, которую вы хотите расширить. Другой способ - использовать команду theme в CUBA CLI.
В результате в проекте будет создана следующая структура каталогов (для расширения темы Halo):
themes/
halo/
branding/
app-icon-login.png
app-icon-menu.png
com.company.application/
app-component.scss
halo-ext.scss
halo-ext-defaults.scss
favicon.ico
styles.scss
Кроме того, скрипт сборки build.gradle будет дополнен задачей buildScssThemes, автоматически запускаемой при сборке модуля web. Опциональная задача deployThemes может быть использована для быстрого применения изменений в темах на работающем приложении.
|
Если ваше приложение включает в себя компонент с расширением темы и вы хотите применить это расширение ко всему приложению, в этом случае необходимо создать расширение темы и для базового проекта. Подробнее о наследовании тем смотрите в разделе Наследование тем из компонентов приложения. |
Можно настроить некоторые параметры фирменного стиля (branding): значки и заголовки окна логина и главного окна, значок вебсайта favicon.ico.
Для использования собственных изображений, замените соответствующие файлы в каталоге modules/web/themes/halo/branding.
Чтобы задать заголовки главного окна, окна логина и текст приглашения окна логина, в CUBA Studio откройте Project properties → Edit и нажмите кнопку Branding внизу страницы. Используйте соответствующие ссылки для задания заголовков окон и текста приглашения окна логина.
Данные параметры сохраняются в главном пакете сообщений модуля web (то есть в файле modules/web/<root_package>/web/messages.properties и его вариантах для разных локалей). Использование пакетов сообщений дает возможность использовать разные файлы изображений для разных локалей пользователей. Пример содержимого файла messages.properties:
application.caption = MyApp
application.logoImage = branding/myapp-menu.png
loginWindow.caption = MyApp Login
loginWindow.welcomeLabel = Welcome to MyApp!
loginWindow.logoImage = branding/myapp-login.png
Путь к favicon.ico указывать не нужно, он должен обязательно находится в корне каталога с именем темы.
В приложение можно добавить собственные шрифты. Для добавления семейства шрифтов импортируйте его в первой строке файла styles.scss, например:
@import url(http://fonts.googleapis.com/css?family=Roboto);
Рассмотрим пример установки желтого цвета фона для поля, отображающего название заказчика.
В XML-дескрипторе экрана определен компонент FieldGroup:
<fieldGroup id="fieldGroup" datasource="customerDs">
<field property="name"/>
<field property="address"/>
</fieldGroup>
Элементы field компонента FieldGroup не имеют атрибута stylename, поэтому необходимо задать имя стиля в контроллере:
@Named("fieldGroup.name")
private TextField nameField;
@Override
public void init(Map<String, Object> params) {
nameField.setStyleName("name-field");
}
В файле halo-ext.scss добавьте определение нового стиля в mixin halo-ext:
@mixin com_company_application-halo-ext {
.name-field {
background-color: lightyellow;
}
}
После пересборки проекта поля будут выглядеть следующим образом:
Изменение существующих стилей компонентов
Для изменения параметров стиля существующих компонентов необходимо добавить соответствующий код CSS в mixin halo-ext файла halo-ext.scss. Например, для того, чтобы пункты главного меню отображались жирным шрифтом, содержимое файла halo-ext.scss должно быть следующим:
@mixin com_company_application-halo-ext {
.v-menubar-menuitem-caption {
font-weight: bold;
}
}
Темы содержат переменные SCSS, которые управляют цветом фона, размерами компонентов, отступами и пр.
Рассмотрим пример расширения темы Halo, так как она основана на теме Valo фреймворка Vaadin, и предоставляет максимальные возможности адаптации.
Файл themes/halo/halo-ext-defaults.scss предназначен для размещения в нем переменных темы. Большинство переменных Halo соответствует описанным в документации по Valo, ниже приведены основные:
$v-background-color: #fafafa; /* component background colour */
$v-app-background-color: #e7ebf2; /* application background colour */
$v-panel-background-color: #fff; /* panel background colour */
$v-focus-color: #3b5998; /* focused element colour */
$v-error-indicator-color: #ed473b; /* empty required fields colour */
$v-line-height: 1.35; /* line height */
$v-font-size: 14px; /* font size */
$v-font-weight: 400; /* font weight */
$v-unit-size: 30px; /* base theme size, defines the height for buttons, fields and other elements */
$v-font-size--h1: 24px; /* h1-style Label size */
$v-font-size--h2: 20px; /* h2-style Label size */
$v-font-size--h3: 16px; /* h3-style Label size */
/* margins for containers */
$v-layout-margin-top: 10px;
$v-layout-margin-left: 10px;
$v-layout-margin-right: 10px;
$v-layout-margin-bottom: 10px;
/* spacing between components in a container (if enabled) */
$v-layout-spacing-vertical: 10px;
$v-layout-spacing-horizontal: 10px;
/* whether filter search button should have "friendly" style*/
$cuba-filter-friendly-search-button: true;
/* whether button that has primary action or marked as primary itself should be highlighted*/
$cuba-highlight-primary-action: false;
/* basic table and datagrid settings */
$v-table-row-height: 30px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 7px;
$v-grid-row-height
$v-grid-row-selected-background-color
$v-grid-cell-padding-horizontal
/* input field focus style */
$v-focus-style: inset 0px 0px 5px 1px rgba($v-focus-color, 0.5);
/* required fields focus style */
$v-error-focus-style: inset 0px 0px 5px 1px rgba($v-error-indicator-color, 0.5);
/* animation for elements is enabled by default */
$v-animations-enabled: true;
/* popup window animation is disabled by default */
$v-window-animations-enabled: false;
/* inverse header is controlled by cuba.web.useInverseHeader property */
$v-support-inverse-menu: true;
/* show "required" indicators for components */
$v-show-required-indicators: false !default;
Пример содержимого файла halo-ext-defaults.scss для темы с темным фоном и немного уменьшенными отступами:
$v-background-color: #444D50;
$v-font-size--h1: 22px;
$v-font-size--h2: 18px;
$v-font-size--h3: 16px;
$v-layout-margin-top: 8px;
$v-layout-margin-left: 8px;
$v-layout-margin-right: 8px;
$v-layout-margin-bottom: 8px;
$v-layout-spacing-vertical: 8px;
$v-layout-spacing-horizontal: 8px;
$v-table-row-height: 25px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 5px;
$v-support-inverse-menu: false;
Изменение заголовка приложения
Тема Halo поддерживает свойство приложения cuba.web.useInverseHeader, управляющее цветом заголовка приложения. По умолчанию это свойство установлено в true, что задает темный (инверсный) заголовок. В проекте можно не изменяя темы сделать заголовок светлым, установив данное свойство в false.
5.5.7.1.3. Создание новой темы
В проекте можно создать одну или несколько новых тем и дать возможность пользователям выбирать среди них подходящую. Создание новой темы позволяет также переопределять переменные файла *-theme.properties, задающие некоторые параметры, требуемые на стороне сервера:
Новые темы можно легко создавать в CUBA Studio, а также в CUBA CLI или вручную. Рассмотрим все три способа создания новой темы на примере темы Hover Dark.
Создание новой темы в CUBA Studio:
Требуемая структура файлов темы будет автоматически создана в модуле web. Модуль webThemesModule и его конфигурация будут автоматически добавлены в скрипты settings.gradle и build.gradle. Сгенерированная задача сборки deployThemes позволит применять изменения в теме без перезапуска сервера приложения.
Создание темы вручную:
web/
src/
themes/
hover-dark/
branding/
app-icon-login.png
app-icon-menu.png
com.haulmont.cuba/
app-component.scss
favicon.ico
hover-dark.scss
hover-dark-defaults.scss
styles.scss
@import "../hover-dark";
@mixin com_haulmont_cuba {
@include hover-dark;
}
@import "../hover/hover";
@mixin hover-dark {
@include hover;
}
@import "hover-dark-defaults";
@import "hover-dark";
.hover-dark {
@include hover-dark;
}
@include=hover-theme.properties
include(":${modulePrefix}-global", ":${modulePrefix}-core", ":${modulePrefix}-web", ":${modulePrefix}-web-themes")
//...
project(":${modulePrefix}-web-themes").projectDir = new File(settingsDir, 'modules/web/themes')
def webThemesModule = project(":${modulePrefix}-web-themes")
configure(webThemesModule) {
apply(plugin: 'java')
apply(plugin: 'maven')
apply(plugin: 'cuba')
appModuleType = 'web-themes'
buildDir = file('../build/scss-themes')
sourceSets {
main {
java {
srcDir '.'
}
resources {
srcDir '.'
}
}
}
}
configure(webModule) {
// . . .
task buildScssThemes(type: CubaWebScssThemeCreation)
task deployThemes(type: CubaDeployThemeTask, dependsOn: buildScssThemes)
assemble.dependsOn buildScssThemes
}
Создание темы с помощью CUBA CLI:
Файловая структура с расширением выбранной темы будет создана в модуле web проекта.
@include=hover-theme.properties
Задачи сборки темы в файлах build.gradle и settings.gradle будут созданы автоматически через CLI.
См. также Создание темы Facebook.
Изменение server-side параметров темы
В теме Halo по умолчанию (при включенном свойстве приложения cuba.web.useFontIcons) значки стандартных действий и экранов платформы загружаются из шрифта Font Awesome. В этом случае можно заменить стандартный значок, задав в файле <your_theme>-theme.properties нужное соответствие между именем значка и именем элемента шрифта. Например, чтобы использовать значок "плюс" для действия create в новой теме Facebook, содержимое файла web/src/facebook-theme.properties должно быть следующим:
@include=halo-theme.properties
cuba.web.icons.create.png = font-icon:PLUS
Фрагмент стандартного экрана списка пользователей в теме Facebook и с измененным значком действия create:
5.5.7.1.4. Наследование тем из компонентов приложения
Если ваш проект включает в себя компонент с новой темой, вы можете настроить использование этой темы во всём проекте.
Чтобы использовать тему из компонента без изменений, просто добавьте её в свойство приложения cuba.themeConfig:
cuba.web.theme = {theme-name}
cuba.themeConfig = havana-theme.properties halo-theme.properties /com/company/{app-component-name}/{theme-name}-theme.properties
Однако, чтобы переопределить некоторые переменные из родительской темы, сначала необходимо создать расширение темы в основном проекте.
В этом примере мы вновь используем тему facebook из предыдущего примера.
themes/
facebook/
branding/
app-icon-login.png
app-icon-menu.png
com.company.application/
app-component.scss
facebook-ext.scss
facebook-ext-defaults.scss
favicon.ico
styles.scss
По умолчанию переменные темы из {theme-name}-ext-defaults не наследуются в проект. Чтобы изменить это поведение, вручную добавьте включение в файл app-component.scss:
@import "facebook-ext";
@import "facebook-ext-defaults";
@mixin com_company_application {
@include com_company_application-facebook-ext;
}
На этом этапе тема facebook уже импортирована в проект из компонента приложения.
cuba.web.theme = facebook
cuba.themeConfig = havana-theme.properties halo-theme.properties /com/company/{app-component-name}/facebook-theme.properties
|
Если при сборке тем возникли проблемы, проверьте каталог modules/web/build/themes-tmp. В нём находятся генерируемые файлы и включения app-component.scss, которые можно использовать для поиска проблем компиляции SCSS. |
5.5.7.1.5. Повторное использование тем
Любую тему можно создать и использовать отдельно от компонента приложения. Для создания темы, которую можно использовать повторно, необходимо создать с нуля отдельный Java-проект и собрать его в единый JAR-файл. Ниже приведена инструкция, как подготовить тему facebook из предыдущих примеров для многократного использования.
halo-facebook/
src/ //sources root
halo-facebook/
com.haulmont.cuba/
app-component.scss
halo-facebook.scss
halo-facebook-defaults.scss
halo-facebook-theme.properties
styles.scss
Этот проект также доступен на GitHub.
allprojects {
group = 'com.haulmont.theme'
version = '0.1'
}
apply(plugin: 'java')
apply(plugin: 'maven')
sourceSets {
main {
java {
srcDir 'src'
}
resources {
srcDir 'src'
}
}
}
rootProject.name = 'halo-facebook'
@import "../halo-facebook";
@mixin com_haulmont_cuba {
@include halo-facebook;
}
@import "../@import "../";
@mixin halo-facebook {
@include halo;
}
@import "../halo/halo-defaults";
$v-background-color: #fafafa;
$v-app-background-color: #e7ebf2;
$v-panel-background-color: #fff;
$v-focus-color: #3b5998;
$v-border-radius: 0;
$v-textfield-border-radius: 0;
$v-font-family: Helvetica, Arial, 'lucida grande', tahoma, verdana, arial, sans-serif;
$v-font-size: 14px;
$v-font-color: #37404E;
$v-font-weight: 400;
$v-link-text-decoration: none;
$v-shadow: 0 1px 0 (v-shade 0.2);
$v-bevel: inset 0 1px 0 v-tint;
$v-unit-size: 30px;
$v-gradient: v-linear 12%;
$v-overlay-shadow: 0 3px 8px v-shade, 0 0 0 1px (v-shade 0.7);
$v-shadow-opacity: 20%;
$v-selection-overlay-padding-horizontal: 0;
$v-selection-overlay-padding-vertical: 6px;
$v-selection-item-border-radius: 0;
$v-line-height: 1.35;
$v-font-size: 14px;
$v-font-weight: 400;
$v-unit-size: 25px;
$v-font-size--h1: 22px;
$v-font-size--h2: 18px;
$v-font-size--h3: 16px;
$v-layout-margin-top: 8px;
$v-layout-margin-left: 8px;
$v-layout-margin-right: 8px;
$v-layout-margin-bottom: 8px;
$v-layout-spacing-vertical: 8px;
$v-layout-spacing-horizontal: 8px;
$v-table-row-height: 25px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 5px;
$v-focus-style: inset 0px 0px 1px 1px rgba($v-focus-color, 0.5);
$v-error-focus-style: inset 0px 0px 1px 1px rgba($v-error-indicator-color, 0.5);
$v-show-required-indicators: true;
@include=halo-theme.properties
gradle assemble install
configure(webModule) {
//...
dependencies {
provided(servletApi)
compile(guiModule)
compile('com.haulmont.theme:halo-facebook:0.1')
themes('com.haulmont.theme:halo-facebook:0.1')
}
//...
}
Если вы установили тему локально, не забудьте добавить локальный репозиторий Maven к списку используемых в проекте репозиториев. Для этого перейдите на вкладку Advanced в окне Studio Project Properties и установите флажок Use local Maven repository.
themes/
halo-facebook/
branding/
app-icon-login.png
app-icon-menu.png
com.company.application/
app-component.scss
halo-ext.scss
halo-ext-defaults.scss
favicon.ico
styles.scss
@import "halo-facebook-defaults";
@import "com.company.application/halo-ext-defaults";
@import "app-components";
@import "com.company.application/halo-ext";
.halo-facebook {
// include auto-generated app components SCSS
@include app_components;
@include com_company_application-halo-ext;
}
cuba.themeConfig = havana-theme.properties halo-theme.properties /halo-facebook/halo-facebook-theme.properties
Теперь тема halo-facebook будет доступна в меню приложения Help > Settings. Вы также можете установить тему по умолчанию, используя свойство приложения cuba.web.theme.
5.5.7.2. Темы в десктоп-приложениях
В десктоп-приложениях базовой темой является тема Nimbus.
Для внесения изменения в стандартную тему нужно создать пакет res.nimbus в пакете com.sample.sales.desktop модуля desktop. В пакете res.nimbus будут храниться файлы темы.
В папке icons хранятся файлы значков, в файле nimbus.xml − описание стиля темы.
В файле свойств для десктоп-приложения нужно установить свойство cuba.desktop.resourceLocations (задает набор директорий, в которых расположены файлы стилей):
cuba.desktop.resourceLocations = \
com/haulmont/cuba/desktop/res \
com/sample/sales/desktop/res
Ниже приведены примеры решения типовых задач.
Добавление значков
Если в десктоп-приложении требуется добавить новый значок, например, для кнопки, нужно создать пакет res.nimbus.icons в пакете com.sample.sales.desktop модуля desktop и поместить в него требуемое изображение.
Описываем кнопку в дескрипторе, указывая в атрибуте icon путь до значка:
<button id="button1" caption="Attention" icon="WARNING"/>
Ниже представлена кнопка со значком attention.png
Переопределение значений свойств темы, установленных по умолчанию
Рассмотрим на примере изменения цвета фона текстовых полей, обязательных для ввода.
В пакете res.nimbus нужно создать файл nimbus.xml следующего содержания:
<theme xmlns="http://schemas.haulmont.com/cuba/desktop-theme.xsd">
<ui-defaults>
<color property="cubaRequiredBackground" value="#f78260"/>
</ui-defaults>
</theme>
Элемент ui-defaults служит для переопределения значений свойств темы платформы, установленных по умолчанию.
В элементе ui-defaults присутствуют как свойства, содержащиеся в стандартной теме Nimbus (http://docs.oracle.com/javase/tutorial/uiswing/lookandfeel/_nimbusDefaults.html), так и свойства, созданные в платформе.
В данном примере переопределено значение свойства платформы cubaRequiredBackground, хранящего цвет фона поля, обязательного для заполнения. Данное изменение коснется всех полей, обязательных для ввода.Переопределение значений свойств темы, установленных по умолчанию.
Создание стиля для элемента с помощью стандартных средств
Рассмотрим пример выделения надписи жирным цветом.
Для того чтобы создать такой стиль, необходимо определить элемент style в файле темы nimbus.xml следующим образом:
<theme xmlns="http://schemas.haulmont.com/cuba/desktop-theme.xsd">
<style name="boldlabel">
<font style="bold"/>
</style>
</theme>
Элемент style может содержать другие элементы, в которых можно определять те или иные свойства: background, foreground, icon.
В описании компонента надписи в xml-дескрипторе, к которой нужно применить созданный стиль, нужно указать атрибут stylename с именем стиля:
<label id="label1" value="msg://labelVal" stylename="boldlabel"/>
Таким образом, данный стиль будет применен только к тем надписям, для которых определен атрибут stylename со значением boldlabel.
Создание пользовательского стиля
Если не хватает стандартных средств изменения стиля компонента, есть возможность создать пользовательский стиль.
Создадим пользовательский стиль, который будет применяться для компонента Button. С помощью стиля надпись на кнопке будет отображаться подчеркнутой.
В первую очередь создадим класс-декоратор UnderlinedLabelDecorator:
public class UnderlinedButtonDecorator implements ComponentDecorator {
@Override
@SuppressWarnings("unchecked")
public void decorate(Object component, Set<String> state) {
DesktopButton item = (DesktopButton) component;
JButton jButton = (JButton) item.getComponent();
Font originalFont = jButton.getFont();
Map attributes = originalFont.getAttributes();
attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
jButton.setFont(originalFont.deriveFont(attributes));
}
}
Определим пользовательский стиль в файле nimbus.xml:
<theme xmlns="http://schemas.haulmont.com/cuba/desktop-theme.xsd">
<style name="button-underlined" component="com.haulmont.cuba.desktop.gui.components.DesktopButton">
<custom class="com.sample.sales.desktop.gui.decorators.UnderlinedButtonDecorator"/>
</style>
</theme>
В атрибуте component элемента style содержится название компонента, к которому может быть применен стиль с названием button-underlined.
В элементе custom указывается путь до класса-декоратора, определенного ранее.
При создании XML-элемента кнопки, к которой нужно применить пользовательский стиль, нужно в атрибуте stylename указать название стиля:
<button stylename="button-underlined" caption="decorated"/>
Кнопка с пользовательским стилем:
В расширенную тему можно также добавить файлы значков для использования в свойствах icon действий и визуальных компонентов, например Button.
Например, чтобы добавить в расширение темы Halo значок, достаточно в описанный в разделе Расширение существующей темы каталог modules/web/themes/halo добавить файл значка (желательно в некоторый подкаталог):
themes/
halo/
icons/
cool-icon.png
В следующих разделах рассматривается использование значков в визуальных компонентах и добавление значков из произвольных библиотек шрифтов.
Наборы значков (icon sets) позволяют отвязать использование значков в визуальных компонентах от конкретных путей к изображениям в теме или констант элементов шрифтов. Кроме того, они упрощают переопределение значков, используемых в UI, унаследованном от компонентов приложения.
Наборы значков - это перечисления (enumerations), каждый элемент которых соответствует некоторому значку. Класс перечисления должен реализовывать интерфейс Icons.Icon с единственным параметром - строкой, задающей источник получения значка, например, font-icon:CHECK или icons/myawesomeicon.png. Для получения источника значка следует использовать бин платформы Icons.
Чтобы наборы значков были доступны из модулей desktop и web, их следует создавать в модуле gui приложения. В случае, если вы планируете использовать только модуль web, наборы значков можно создавать в веб-модуле. Имена элементов перечисления в наборе должны соответствовать регулярному выражению [A-Z]_, то есть содержать только заглавные буквы и нижнее подчёркивание.
Пример набора значков:
public enum MyIcon implements Icons.Icon {
// adding new icon
COOL_ICON("icons/cool-icon.png"),
// overriding a CUBA default icon
OK("icons/my-ok.png");
protected String source;
MyIcon(String source) {
this.source = source;
}
@Override
public String source() {
return source;
}
}
Наборы значков необходимо зарегистрировать в свойстве приложения cuba.iconsConfig, например:
web-app.properties
cuba.iconsConfig = +com.company.demo.gui.icons.MyIcon
|
Чтобы наборы значков из компонента приложения были доступны в проекте, необходимо добавить данное свойство приложения в дескриптор компонента. |
Теперь вы можете использовать значки из созданного набора, декларативно указывая имя соответствующего элемента enum в XML-дескрипторе:
<button icon="COOL_ICON"/>
или программно в контроллере экрана:
button.setIconFromSet(MyIcon.COOL_ICON);
Используя специальные префиксы, вы можете декларативно использовать значки из разных источников:
<button icon="theme:awesomeFolder/superIcon.png"/>
<button icon="file:D:/superIcon.png"/>
<button icon="classpath:/com/company/demo/web/superIcon.png"/>
В платформе доступен уже готовый набор значков - CubaIcon. Он включает в себя практически все значки из библиотеки FontAwesome, а также собственные значки CUBA. Значки из этого набора по умолчанию можно выбрать в редакторе Studio:
5.5.8.2. Добавление значков из других библиотек шрифтов
Для более тонкой настройки расширенной темы можно создать значки, встроенные в шрифты, либо использовать готовые внешние библиотеки значков.
import com.vaadin.server.FontIcon;
import com.vaadin.server.GenericFontIcon;
public enum IcoMoon implements FontIcon {
HEADPHONES(0XE900),
SPINNER(0XE905);
public static final String FONT_FAMILY = "IcoMoon";
private int codepoint;
IcoMoon(int codepoint) {
this.codepoint = codepoint;
}
@Override
public String getFontFamily() {
return FONT_FAMILY;
}
@Override
public int getCodepoint() {
return codepoint;
}
@Override
public String getHtml() {
return GenericFontIcon.getHtml(FONT_FAMILY, codepoint);
}
@Override
public String getMIMEType() {
throw new UnsupportedOperationException(FontIcon.class.getSimpleName()
+ " should not be used where a MIME type is needed.");
}
public static IcoMoon fromCodepoint(final int codepoint) {
for (IcoMoon f : values()) {
if (f.getCodepoint() == codepoint) {
return f;
}
}
throw new IllegalArgumentException("Codepoint " + codepoint
+ " not found in IcoMoon");
}
}
Файлы шрифта включают в себя набор следующих расширений:
Использованный в этом примере набор шрифтов icomoon из открытой библиотеки представлен в виде 4 файлов: icomoon.eot, icomoon.svg, icomoon.ttf, icomoon.woff, которые используются совместно.
@mixin icomoon-style {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@font-face {
font-family: 'icomoon';
src:url('icomoon.eot?hwgbks');
src:url('icomoon.eot?hwgbks#iefix') format('embedded-opentype'),
url('icomoon.ttf?hwgbks') format('truetype'),
url('icomoon.woff?hwgbks') format('woff'),
url('icomoon.svg?hwgbks#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
.IcoMoon {
@include icomoon-style;
}
@import "fonts/icomoon/icomoon";
import com.haulmont.cuba.gui.icons.Icons;
public enum IcoMoonIcon implements Icons.Icon {
HEADPHONES("ico-moon:HEADPHONES"),
SPINNER("ico-moon:SPINNER");
protected String source;
IcoMoonIcon(String source) {
this.source = source;
}
@Override
public String source() {
return source;
}
}
Для работы с наборами значков в платформе есть механизм, основанный на использовании IconProvider и IconResolver.
IconProvider - это интерфейс-маркер, доступный только в веб-модуле, который предоставляет доступ к ресурсу (com.vaadin.server.Resource) по переданному пути.
Бин IconResolver проходится по всем бинам, реализующим IconProvider, в поисках того, кто может предоставить ресурс к данному значку.
На самом деле, в платформе есть два интерфейса IconResolver и их реализации для модулей desktop и web. Оба они являются бинами-фасадами, которые принимают путь к значку и возвращают ресурс для своего модуля:
Чтобы использовать этот механизм, необходимо создать собственную реализацию IconProvider, например, так:
import com.haulmont.cuba.web.gui.icons.IconProvider;
import com.vaadin.server.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Order(10)
@Component
public class IcoMoonIconProvider implements IconProvider {
private final Logger log = LoggerFactory.getLogger(IcoMoonIconProvider.class);
@Override
public Resource getIconResource(String iconPath) {
Resource resource = null;
iconPath = iconPath.split(":")[1];
try {
resource = ((Resource) IcoMoon.class
.getDeclaredField(iconPath)
.get(null));
} catch (IllegalAccessException | NoSuchFieldException e) {
log.warn("There is no icon with name {} in the FontAwesome icon set", iconPath);
}
return resource;
}
@Override
public boolean canProvide(String iconPath) {
return iconPath.startsWith("ico-moon:");
}
}
Здесь мы явно назначаем порядок для этого бина аннотацией @Order.
cuba.iconsConfig = +com.company.demo.gui.icons.IcoMoonIcon
Теперь вы можете использовать значки по прямой ссылке на класс и элемент enum в XML-дескрипторе экрана:
<button caption="Headphones" icon="ico-moon:HEADPHONES"/>
или в контроллере Java:
spinnerBtn.setIconFromSet("ico-moon:SPINNER");
В результате, новые значки добавились к кнопкам:
Переопределение значков с помощью наборов
Механизм наборов значков позволяет переопределять некоторые значки из других наборов. Для этого необходимо создать и зарегистрировать новый набор значков (enumeration) с теми же именами значков (options), но с другими путями (source). В примере ниже создан новый набор MyIcon, в котором переопределены стандартные значки из набора CubaIcon.
public enum CubaIcon implements Icons.Icon {
OK("font-icon:CHECK"),
CANCEL("font-icon:BAN"),
...
}
public enum MyIcon implements Icons.Icon {
OK("icons/my-custom-ok.png"),
...
}
cuba.iconsConfig = +com.company.demo.gui.icons.MyIcon
Теперь вместо стандартного значка OK будет использовано новое изображение:
Icons icons = AppBeans.get(Icons.NAME);
button.setIcon(icons.getIcon(CubaIcon.OK))
При необходимости игнорировать переопределение и использовать стандартные значки, просто используйте путь к значку вместо имени элемента перечисления:
<button caption="Created" icon="icons/create.png"/>
или
button.setIcon(CubaIcon.CREATE_ACTION.source());
Реализация универсального пользовательского интерфейса в блоке Web Client основана на фреймворке Vaadin. Рассмотрим основные классы, входящие в состав инфраструктуры веб-клиента.
Рисунок 23. Классы инфраструктуры Web Client
<bean name="cuba_App" class="com.company.sample.web.MyApp" scope="vaadin"/>
Без переопределения главного окна можно управлять некоторыми параметрами с помощью следующих свойств приложения:
Для того, чтобы обрабатывать нажатия на кнопку Back браузера, реализуйте интерфейс CubaHistoryControl.HistoryBackHandler в ваших TopLevelWindow (окно логина и главное окно). Метод onHistoryBackPerformed() этого интерфейса вызывается вместо стандартного поведения браузера, если свойство приложения cuba.web.allowHandleBrowserHistoryBack установлено в true.
5.5.9.1. Работа с компонентами Vaadin
Для работы непосредственно с компонентами Vaadin, реализующими интерфейсы библиотеки визуальных компонентов в блоке Web Client, воспользуйтесь следующими методами интерфейса Component:
Методы принимают класс компонента, который нужно вернуть, например:
com.vaadin.ui.TextField vTextField = textField.unwrap(com.vaadin.ui.TextField.class);
Можно также использовать статические методы unwrap() и getComposition() класса WebComponentsHelper, передавая в них CUBA-компонент.
Следует иметь в виду, что если экран расположен в модуле gui проекта, то в его контроллере можно работать только с обобщенными интерфейсами CUBA-компонентов. Чтобы использовать unwrap(), нужно либо расположить весь экран в модуле web, либо воспользоваться механизмом компаньонов контроллеров.
5.5.9.2. Атрибуты DOM и CSS для визуальных компонентов
Платформа CUBA предоставляет специальный API для HTML-атрибутов, позволяющий устанавливать DOM и CSS атрибуты для визуальных компонентов.
DOM/CSS атрибуты можно установить программно с помощью бина HtmlAttributes и следующих его методов:
Имена наиболее часто используемых DOM-атрибутов и CSS-свойств доступны как константы класса HtmlAttributes, однако вы можете использовать и свои собственные имена атрибутов.
|
Будет ли атрибут работать с конкретным компонентом, зависит от этого компонента. Некоторые визуальные компоненты могут скрыто использовать те же атрибуты для своих собственных нужд, поэтому приведённые выше методы в определенных случаях могут не работать. |
Чтобы использовать бин HtmlAttributes, его нужно инжектировать в контроллер экрана, например:
XML descriptor
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
class="com.company.demo.web.screens.DemoScreen"
caption="Demo">
<layout>
<button caption="Demo"
id="demoBtn"
width="33%"/>
</layout>
</window>
Screen controller
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.components.HtmlAttributes;
import javax.inject.Inject;
import java.util.Map;
public class DemoScreen extends AbstractWindow {
@Inject
protected Button demoBtn;
@Inject
protected HtmlAttributes html;
@Override
public void init(Map<String, Object> params) {
super.init(params);
html.setDomAttribute(demoBtn, HtmlAttributes.DOM.TITLE, "Hello !");
html.setCssProperty(demoBtn, HtmlAttributes.CSS.BACKGROUND_COLOR, "red");
html.setCssProperty(demoBtn, HtmlAttributes.CSS.BACKGROUND_IMAGE, "none");
html.setCssProperty(demoBtn, HtmlAttributes.CSS.BOX_SHADOW, "none");
html.setCssProperty(demoBtn, HtmlAttributes.CSS.BORDER_COLOR, "red");
html.setCssProperty(demoBtn, "color", "white");
html.setCssProperty(demoBtn, HtmlAttributes.CSS.MAX_WIDTH, "400px");
}
}
5.5.9.3. Компоновка главного окна приложения
Механизм предоставляет возможность задавать компоновку главного экрана веб-приложения с использованием технологии универсального пользовательского интерфейса CUBA - XML-дескриптора и Java-контроллера с применением визуальных компонентов и источников данных.
Главное окно - особый экран системы, имеющий идентификатор mainWindow. Контроллер главного экрана должен быть наследником класса AbstractMainWindow.
Помимо стандартных компонентов GUI в главном экране приложения можно использовать дополнительные компоненты:
Метод setUserNameFormatter() используется для отображения имени пользователя в виде, отличном от стандартного имени экземпляра сущности User:
userIndicator.setUserNameFormatter(value -> value.getName() + " - [" + value.getEmail() + "]");
Для работы с дополнительными компонентами в XML-дескриптор экрана нужно добавить элемент xmlns:main:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:main="http://schemas.haulmont.com/cuba/mainwindow.xsd"
class="com.company.sample.gui.MainWindow">
<layout>
</layout>
</window>
Специальный компонент AppWorkArea представляет собой рабочую область, в которой открываются экраны приложения. Если свойство приложения cuba.web.appWindowMode имеет значение TABBED (по умолчанию), то на месте рабочей области будет расположен компонент TabSheet с экранами приложения. В противном случае рабочая область будет содержать единственный открытый экран. Свойства приложения cuba.web.mainTabSheetMode и cuba.web.managedMainTabSheetMode определяют, как будет обрабатываться содержимое вкладок при их переключении. Когда не открыт ни один экран, рабочая область содержит компоненты, определенные во вложенном элементе initialLayout:
<main:workArea id="workArea" width="100%" height="100%">
<main:initialLayout spacing="true" margin="true">
<!-- content shown when there are no open screens -->
</main:initialLayout>
</main:workArea>
При открытии экранов компоновка начального экрана (initialLayout) удаляется из AppWorkArea, при закрытии всех экранов - добавляется обратно. Для реакции на события смены рабочей области на стартовый экран и на отображение экранов приложения можно добавить обработчик AppWorkArea.StateChangeListener. Например, в таком слушателе можно разместить код обновления данных стартового экрана.
В платформе существуют 2 стандартные реализации главного окна приложения. XML-дескриптор классического окна с верхним горизонтальным меню - /com/haulmont/cuba/web/app/mainwindow/mainwindow.xml, соответствующий контроллер - AppMainWindow. Также доступна компоновка с вертикальным боковым меню.
Стандартная реализация главного окна может быть расширена в проекте, так же как обычный экран системы. Пример расширяющего экрана:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
extends="com/haulmont/cuba/web/app/mainwindow/mainwindow.xml"
class="com.haulmont.cuba.web.app.mainwindow.AppMainWindow">
<layout>
<vbox ext:index="0">
<label value="This is my main window!" stylename="h2"/>
</vbox>
</layout>
</window>
Этот экран должен быть зарегистрирован в web-screens.xml с идентификатором mainWindow.
Самый простой способ расширить главное окно - использовать визуальный дизайнер Generic UI templates в CUBA Studio: нажмите New на вкладке GENERIC UI на панели навигации и выберите шаблон нового главного окна. Новый файл ext-mainwindow.xml будет создан в модуле Web и автоматически зарегистрирован web-screens.xml.
Реализация главного окна может быть полностью заменена. Например:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:main="http://schemas.haulmont.com/cuba/mainwindow.xsd"
class="com.company.sample.gui.MainWindow">
<layout expand="middlePanel">
<hbox margin="true"
stylename="gray"
width="100%">
<label align="MIDDLE_CENTER"
value="Header"/>
</hbox>
<main:menu width="100%"/>
<split id="middlePanel"
orientation="horizontal"
pos="80"
width="100%">
<main:workArea id="workArea"
height="100%"
width="100%">
<main:initialLayout stylename="red">
<label align="MIDDLE_CENTER"
value="Work Area (Initial Layout)"/>
</main:initialLayout>
</main:workArea>
<main:foldersPane height="100%"
stylename="blue"
width="100%"/>
</split>
<hbox margin="true"
stylename="gray"
width="100%">
<label align="MIDDLE_CENTER"
value="Footer"/>
</hbox>
</layout>
</window>
В результате главное окно приложения выглядит следующим образом:
Оно же с открытым экраном:
Свойство приложения cuba.web.showBreadCrumbs позволяет скрыть панель навигации (breadcrumbs) над открытым экраном.
5.5.9.4. Процесс входа в Web Client
В данном разделе описывается, как работает аутентификация на веб-клиент и как расширить ее в проекте. Для информации об аутентификации на среднем слое см. Вход в систему.
Реализация логина в Web Client включает следующие механизмы:
Рисунок 24. Механизмы логина в Web Client
Основной интерфейс подсистемы входа в Web Client - Connection, включающий следующие основные методы:
После успешного входа Connection устанавливает объект UserSession в атрибут VaadinSession и устанавливает SecurityContext. Объект Connection связан с VaadinSession, поэтому вы не можете использовать его из фоновых потоков, при попытке вызова login/logout из фонового потока выбрасывается исключение IllegalConcurrentAccessException.
Обычно, логин выполняется из экрана AppLoginWindow, который поддерживает вход при помощи логина/пароля и токена "запомнить меня".
Реализация Connection по умолчанию - ConnectionImpl, который делегирует логин цепочке объектов LoginProvider. Интерфейс LoginProvider предназначен для реализации модулей входа, которые могут обрабатывать специфичные реализации интерфейса Credentials, также этот интерфейс включает метод supports(), позволяющий проверить поддерживает ли модули определённый тип Credentials.
Рисунок 25. Стандартный процесс входа в Web Client
Стандартный процесс входа:
Все реализации LoginProvider должны:
HttpRequestFilter - маркерный интерфейс для бинов, которые будут автоматически добавлены в цепочку фильтров приложения в качестве HTTP фильтра: https://docs.oracle.com/javaee/6/api/javax/servlet/Filter.html. Вы можете использовать такой фильтр для реализации дополнительной аутентификации, пре- и пост-обработки запроса и ответа.
Вы можете реализовать такой Filter если создадите компонент Spring Framework и реализуете интерфейс HttpRequestFilter:
@Component
public class CustomHttpFilter implements HttpRequestFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// delegate to the next filter/servlet
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
Обратите внимание, что минимальная реализация должна делегировать исполнение объекту FilterChain, в противном случае ваше приложение будет неработоспособно. По умолчанию фильтры добавленные как бины HttpRequestFilter не будут получать запросы к каталогу VAADIN и другим путям, указанным в свойстве приложения cuba.web.cubaHttpFilterBypassUrls.
Платформа включает следующие реализации интерфейса LoginProvider:
Все реализации создают активную сессию при помощи AuthenticationService.login().
Вы можете переопределить любой из провайдеров входа при помощи механизмов Spring Framework.
Стандартная реализация Connection - ConnectionImpl публикует следующие события во время процедуры входа:
Обработчики событий BeforeLoginEvent и LoginFailureEvent могут выбросить исключение LoginException чтобы прервать процесс входа или переопределить оригинальную причину ошибки входа.
Например, при помощи обработчика BeforeLoginEvent вы можете разрешить вход в Web Client только для пользователей, логин которых включает домен компании.
@Component
public class BeforeLoginEventListener {
@Order(10)
@EventListener
protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException {
if (event.getCredentials() instanceof LoginPasswordCredentials) {
LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) event.getCredentials();
if (loginPassword.getLogin() != null
&& !loginPassword.getLogin().contains("@company")) {
throw new LoginException(
"Only users from @company are allowed to login");
}
}
}
}
Дополнительно, стандартный класс приложения - DefaultApp публикует следующие события:
Событие AppStartedEvent может использоваться для реализации прозрачного входа и SSO со сторонними системами, такими как Jasig CAS. Обычно, используется вместе с дополнительной реализацией HttpRequestFilter, которая должна собрать и предоставить дополнительные аутентификационные данные из HTTP запроса.
Допустим, что система должна автоматически выполнять вход для пользователей, у которых есть специальный файл cookie - PROMO_USER.
@Order(10)
@Component
public class AppStartedEventListener implements ApplicationListener<AppStartedEvent> {
private static final String PROMO_USER_COOKIE = "PROMO_USER";
@Inject
private Logger log;
@Override
public void onApplicationEvent(AppStartedEvent event) {
String promoUserLogin = event.getApp().getCookieValue(PROMO_USER_COOKIE);
if (promoUserLogin != null) {
Connection connection = event.getApp().getConnection();
if (!connection.isAuthenticated()) {
try {
connection.login(new ExternalUserCredentials(promoUserLogin));
} catch (LoginException e) {
log.warn("Unable to login promo user {}: {}", promoUserLogin, e.getMessage());
} finally {
event.getApp().removeCookie(PROMO_USER_COOKIE);
}
}
}
}
}
Так, если веб-браузер хранит файл cookie PROMO_USER, и пользователь откроет приложение, будет выполнен вход от имени пользователя, указанного в promoUserLogin.
Если вы хотите выполнить дополнительные действия после входа в приложение и инициализации UI вы можете реализовать обработчик события AppLoggedInEvent. Обратите внимание, что вы должны проверить, аутентифицирован ли пользователь или нет в обработчиках события, поскольку все события публикуются и для пользователя anonymous, даже если пользователь не аутентифицирован.
Вы можете расширить механизм входа, используя следующие точки расширения
Вы можете заменить существующие бины, используя механизмы Spring Framework, например, зарегистрировав новый бин в конфигурационном файле Spring XML модуля web.
<bean id="cuba_LoginPasswordLoginProvider"
class="com.company.demo.web.CustomLoginProvider"/>
При необходимости можно создать собственный класс имплементации CubaAuthProvider и использовать его, установив следующие свойства приложения:
cuba.web.externalAuthentication = true
cuba.web.externalAuthenticationProviderClass = com.company.sample.web.MyAuthProvider
Следующие компоненты считаются устаревшими:
Не используйте эти компоненты. Они будут удалены в следующей major версии платформы.
Используйте вместо этих механизмов точки расширения Web Client.
5.5.10. Специфика Desktop Client
Реализация универсального пользовательского интерфейса в блоке Desktop Client основана на Java Swing. Рассмотрим основные классы, входящие в состав инфраструктуры десктоп клиента.
Рисунок 26. Классы инфраструктуры Desktop Client
В конкретном приложении необходимо создать собственный класс-наследник App и переопределить в нем следующие методы:
@Override
protected String getDefaultAppPropertiesConfig() {
return "/cuba-desktop-app.properties /desktop-app.properties";
}
@Override
protected String getDefaultHomeDir() {
return System.getProperty("user.home") + "/.mycompany/sales";
}
@Override
protected String getDefaultLogConfig() {
return "sales-logback.xml";
}
Кроме того, в собственном классе-наследнике App необходимо определить метод main() следующим образом:
public static void main(final String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
app = new App();
app.init(args);
app.show();
app.showLoginDialog();
}
});
}
При отделении пользователем вкладок главного окна или компонента TabSheet (см. атрибут detachable) создаются дополнительные экземпляры TopLevelFrame, не содержащие главного меню.
Как правило, WindowManager не используется в прикладном коде напрямую.
5.5.10.1. Работа с компонентами Swing
Для работы непосредственно с компонентами Swing, реализующими интерфейсы библиотеки визуальных компонентов в блоке Desktop Client, воспользуйтесь следующими методами интерфейса Component:
Методы принимают класс компонента, который нужно вернуть, например:
javax.swing.JButton jButton = button.unwrap(javax.swing.JButton.class);
Можно также использовать статические методы unwrap() и getComposition() класса DesktopComponentsHelper, передавая в них CUBA-компонент.
Следует иметь в виду, что если экран расположен в модуле gui проекта, то в его контроллере можно работать только с обобщенными интерфейсами CUBA-компонентов. Чтобы использовать unwrap(), нужно либо расположить весь экран в модуле desktop, либо воспользоваться механизмом компаньонов контроллеров.
5.5.11. Собственные визуальные компоненты
В данном разделе содержится обзор различных способов создания собственных визуальных компонентов в CUBA-приложениях. Практическое руководство по использованию этих подходов содержится в разделе Создание собственных визуальных компонентов.
Новый визуальный компонент может быть создан с помощью следующих технологий:
Это простейший способ, требующий следующих шагов:
См. пример в разделе Подключение аддона Vaadin.
Данный метод рекомендуется, если у вас уже есть подходящий компонент, написанный на JavaScript. Чтобы использовать его в приложении, требуется следующее:
См. пример в разделе Подключение JavaScript библиотеки.
Данный метод рекомендуется для создания полностью новых визуальных компонентов. Он требует следующих шагов:
См. пример в разделе Создание GWT компонента.
Степень интегрированности визуального компонента в платформу можно разделить на три уровня:
5.5.11.1. Использование ресурсов WebJar
Данный метод позволяет использовать различные JavaScript-библиотеки, упакованные в JAR-файлы и развёрнутые в Maven Central. Для подключения библиотеки к приложению требуется следующее:
compile 'org.webjars.bower:jrcarousel:1.0.0'
Аннотация может использоваться только с наследниками ClientConnector (которые обычно являются классами компонентов UI в модуле web-toolkit).
Значение аннотации @WebJarResource, или определение ресурса, можно указать в одном из двух форматов:
@WebJarResource("pivottable:plugins/c3/c3.min.css")
@WebJarResource("jquery-ui/1.12.1/jquery-ui.min.js")
Оно может содержать одно или более строковых определений ресурсов WebJar:
@WebJarResource({
"jquery-ui:jquery-ui.min.js",
"jquery-fileupload:jquery-fileupload.min.js",
"jquery-fileupload:jquery-fileupload.min.js"
})
public class CubaFileUpload extends CubaAbstractUploadComponent {
...
}
Указывать версию WebJar не нужно, так как согласно стратегии управления версиями Maven будет автоматически использован WebJar с самым большим номером версии.
Дополнительно можно указать путь к каталогу внутри VAADIN/webjars/, из которого должны подгружаться статические ресурсы. В такой каталог вы можете сами помещать новые версии ресурсов, и они будут автоматически переопределять используемые WebJar. Для указания каталога используйте свойство overridePath аннотации @WebJarResource, к примеру:
@WebJarResource(value = "pivottable:plugins/c3/c3.min.css", overridePath = "pivottable")
5.5.12. Подключаемые фабрики компонентов
Механизм подключаемых фабрик компонентов расширяет процедуру генерации компонентов и позволяет создавать различные поля редактирования в FieldGroup, Table и DataGrid. Это означает, что компоненты приложения или сам ваш проект могут предоставлять собственные стратегии, которые будут создавать нестандартные компоненты и/или поддерживать кастомные типы данных.
Для того, чтобы воспользоваться данным механизмом, следует использовать метод ComponentsFactory.createComponent(ComponentGenerationContext). Он работает следующим образом:
Реализации интерфейса ComponentGenerationStrategy используются при создании UI компонентов. Проект может содержать любое количество таких стратегий.
ComponentGenerationContext - класс, содержащий следующую информацию, которая может быть использована при создании компонента:
В платформе существуют две стандартных реализации ComponentGenerationStrategy:
Пример ниже показывает, как заменить стандартную генерацию компонента в FieldGroup для определенного атрибута некоторой сущности.
@Component(SalesComponentGenerationStrategy.NAME)
public class SalesComponentGenerationStrategy implements ComponentGenerationStrategy, Ordered {
public static final String NAME = "sales_SalesComponentGenerationStrategy";
@Inject
private ComponentsFactory componentsFactory;
@Inject
private Metadata metadata;
@Nullable
@Override
public Component createComponent(ComponentGenerationContext context) {
String property = context.getProperty();
MetaClass orderMetaClass = metadata.getClassNN(Order.class);
// Check the specific field of the Order entity
// and that the component is created for the FieldGroup component
if (orderMetaClass.equals(context.getMetaClass())
&& "date".equals(property)
&& context.getComponentClass() != null
&& FieldGroup.class.isAssignableFrom(context.getComponentClass())) {
DatePicker datePicker = componentsFactory.createComponent(DatePicker.class);
Datasource datasource = context.getDatasource();
if (datasource != null) {
datePicker.setDatasource(datasource, property);
}
return datePicker;
}
return null;
}
@Override
public int getOrder() {
return 50;
}
}
|
Обратите внимание, что переопределение существующих стратегий генерации компонентов может приводить к ошибкам в случае изменения типа возвращаемого компонента, поскольку некоторые контроллеры экрана могут иметь код, ожидающий определенный тип компонента. Например, в случае использования вышеприведенной стратегии, следующая инжекция приведет к исключению: @Named("fieldGroup.date") private DateField dateField; Если вы попытаетесь открыть такой экран, то получите следующее исключение: IllegalArgumentException: Can not set com.haulmont.cuba.gui.components.DateField field com.company.sales.web.order.OrderEdit.dateField to com.haulmont.cuba.web.gui.components.WebDatePicker |
Пример ниже показывает, как определить ComponentGenerationStrategy для специализированного datatype.
@Order(100)
@Component(ColorComponentGenerationStrategy.NAME)
public class ColorComponentGenerationStrategy implements ComponentGenerationStrategy {
public static final String NAME = "colordatatype_ColorComponentGenerationStrategy";
@Inject
private ComponentsFactory componentsFactory;
@Nullable
@Override
public Component createComponent(ComponentGenerationContext context) {
String property = context.getProperty();
MetaPropertyPath mpp = resolveMetaPropertyPath(context.getMetaClass(), property);
if (mpp != null) {
Range mppRange = mpp.getRange();
if (mppRange.isDatatype()
&& ((Datatype) mppRange.asDatatype()) instanceof ColorDatatype) {
ColorPicker colorPicker = componentsFactory.createComponent(ColorPicker.class);
colorPicker.setDefaultCaptionEnabled(true);
Datasource datasource = context.getDatasource();
if (datasource != null) {
colorPicker.setDatasource(datasource, property);
}
return colorPicker;
}
}
return null;
}
protected MetaPropertyPath resolveMetaPropertyPath(MetaClass metaClass, String property) {
MetaPropertyPath mpp = metaClass.getPropertyPath(property);
if (mpp == null && DynamicAttributesUtils.isDynamicAttribute(property)) {
mpp = DynamicAttributesUtils.getMetaPropertyPath(metaClass, property);
}
return mpp;
}
}
В данном разделе приведена информация обо всех горячих клавишах (shortcuts), которые используются по умолчанию в универсальном пользовательском интерфейсе приложения. Все перечисленные ниже свойства приложения принадлежат интерфейсу ClientConfig и используются в блоках Web Client и Desktop Client.
В полях выбора кроме вышеперечисленных горячих клавиш поддерживается вызов действий сочетанием CTRL-ALT-1, CTRL-ALT-2 и так далее по количеству действий. То есть при нажатии сочетания клавиш CTRL-ALT-1 произойдет вызов действия, которое описано первым в списке действий, при нажатии сочетания клавиш CTRL-ALT-2 − вызов второго действия и так далее. Сочетание CTRL-ALT можно заменить другим, указав его в свойстве приложения cuba.gui.pickerShortcut.modifiers.
5.6. Пользовательский интерфейс на Polymer
Клиентский блок пользовательского интерфейса на Polymer предоставляет возможность быстрого создания front-end порталов с mobile-first responsive веб-интерфейсом. Он основан на фреймворке Google Polymer и обеспечивает тесную интеграцию с мобильными браузерами для добавления веб-приложений на home screen устройстваи для работы оффлайн.
Polymer UI платформы CUBA имеет следующие особенности:
На данный момент наш подход заключается в максимальном использовании методов и инструментов, предоставляемых командой Polymer для создания Progressive Web Apps. Мы стараемся не отходить далеко от примеров и практик описанных в документации Polymer, чтобы снизить порог вхождения и унифицировать процесс изучения технологии. Приложения на базе Polymer не только используют Веб-компоненты, но и сами состоят из них.
По умолчанию, Studio генерирует пользовательский интерфейс на основе paper-elements - набора элементов от Polymer, который построен на принципах material design от Google. Также можно использовать и другие компоненты, создав для них свои шаблоны в Studio.
Для эффективной разработки необходимо ознакомиться с основами Polymer: https://github.com/Polymer/polymer#polymer-in-1-minute. Предпочтительнее изучить тему глубже: https://www.polymer-project.org/2.0/start/. Так как Polymer строится вокруг Web стандартов, изучая его, вы, во многом, изучаете саму веб-платформу.
Git должен быть установлен и доступен из командной строки. Он необходим для bower - системы управления пакетами для front-end приложений.
5.6.2. Поддерживаемые браузеры
См. список поддерживаемых браузеров на сайте Polymer.
|
Не рекомендуется использовать Polymer-клиент, если в вашем проекте требуется поддержка устаревших браузеров. |
Для добавления в проект клиентского модуля Polymer откройте его в CUBA Studio и нажмите Create module > Create polymer client module на вкладке Project Properties навигатора. Studio создаст модуль polymer-client и сконфигурирует соответствующим образом build.gradle. Модуль будет содержать заглушку приложения, которая позволяет подключаться к REST API и выполнять login и logout пользователя в middleware.
После создания модуля, запустите сервер приложения и откройте http://localhost:8080/app-front в веб-браузере. Вы увидите форму логина. После успешного входа будет отображено главное окно с вертикальным меню и responsive дизайном.
Для создания экрана UI, работающего с сущностью, выберите сущность в навигаторе Studio и нажмите New > Polymer UI component. Выберите шаблон (например, Entity cards list with editor), заполните свойства шаблона и нажмите Create. Веб-компонент будет сгенерирован и добавлен в меню приложения. Studio обеспечивает hot-deploy компонентов Polymer, так что вам необходимо лишь обновить страницу в браузере и вы увидите в меню элемент, вызывающий только что созданный экран.
5.6.4. Структура проекта и система сборки
В системе сборки Polymer-клиента используются следующие инструменты:
По умолчанию, установкой и запуском этих инструментов занимается Gradle, однако можно использовать их и напрямую, см. Использование нативных инструментов Polymer.
Polymer 2.0 и нативные компоненты написаны с использованием синтаксиса ES6, поэтому для поддержки старых браузеров требуется дополнительная компиляция ES6 → ES5 при сборке.
|
По умолчанию в Polymer клиенте используется пресет сбоки es6-unbundled, который не подразумевает компиляцию в ES5. Поэтому для поддержки старых проектов и развёртывания на живые сервера необходимо изменить его на es5-bundled. |
Для изменения пресета отредактируйте свойство builds в файле polymer.json:
"builds": [
{
"preset": "es5-bundled",
"basePath": "/app-front/",
"addServiceWorker": false
}
]
Более подробную информацию о пресетах и опциях сборки вы можете найти на сайте Polymer.
В polymer.json можно указать несколько вариантов сборки, для разворачивания конкретного варианта в Tomcat необходимо изменить задачу deploy в build.gradle:
task deploy(type: Copy, dependsOn: [assemble, deployUnbundled]) {
from file('build/es5-unbundled')
into "$cuba.tomcat.dir/webapps/$frontAppDir"
}
Обратите внимание на изменение es6-bundled → es5-unbundled в polymer.json и build.gradle.
polymer-client/
|-- src/
| |-- app-shell.html
| |-- shared-styles.html
|-- images
| |-- app-icon/
| |-- favicon.ico
|-- .gitignore
|-- bower.json
|-- index.html
|-- manifest.json
|-- package.json
|-- polymer.json
|-- service-worker.js
|-- sw-precache-config.js
src
Директория, содержащая веб-компоненты.
package.json
Список модулей Node.js, используемых при сборке.
bower.json
Список зависимостей, используемых в самом приложении (преимущественно веб-компоненты).
polymer.json
Конфигурация сборки Polymer.
index.html
Входная точка приложения. Содержит логику загрузки полифилов и импорт <appname>-shell.html.
manifest.json
Web app manifest. Содержит информацию, используемую при добавлении приложения на домашний экран мобильного устройства. Больше информации здесь: https://developer.mozilla.org/en-US/docs/Web/Manifest
service-worker.js
Заглушка Service worker.
sw-precache-config.js
Файл конфигурации, используемый библиотекой sw-precache для генерации service worker при сборке. По умолчанию отключено. См. Использование offline.
При запуске и развёртывании приложений из CUBA Studio или с помощью Gradle система сборки упакует компоненты в бандлы в соответствии с конфигурацией в polymer.json. По умолчанию, всё приложение упаковывается в один файл <appname>-shell.html. Если проект запущен, то при изменении компонентов Studio автоматически копирует их в Tomcat. Также она заменит собранный бандл <appname>-shell.html на его исходную версию, чтобы подтягивались изменения в отдельных компонентах. Необходимо обратить на это внимание при развёртывании приложений в production напрямую из tomcat/webapps.
|
Если вы используете пресет es5-bundled, то hot deploy из Studio работать не будет, т.к. Studio не производит транспиляцию JavaScript на лету. |
|
Если вы используете клиент на базе TypeScript, то вам необходимо вручную выполнить команду npm run watch, чтобы изменения в классах компонентов подтягивались в hot deploy. |
5.6.4.3. Использование нативных инструментов Polymer
Вы можете использовать нативный инструментарий фреймворка Polymer. Это может быть удобно, если над проектом работает отдельная команда front-end разработчиков. В этом случае, в системе должен быть установлен Node.js.
Установите bower и gulp глобально:
npm install bower polymer-cli -g
Теперь вы можете собирать и запускать веб-приложение без Gradle:
cd modules/polymer-client
npm install
bower install
polymer serve
Чтобы запускать приложение на dev сервере Polymer вместо Tomcat, внесите следующие изменения:
<myapp-shell api-url="http://localhost:8080/app/rest/"></myapp-shell>
Теперь приложение будет доступно по адресу http://localhost:8081 (точный порт будет указан в консоли), а доступ к его REST API будет осуществляться по http://localhost:8080/app/rest/.
Подробный справочник по API CUBA-элементов находится здесь.
Для того, чтобы использовать cuba- элементы, необходимо инициализировать подключение к REST API с помощью элемента cuba-app:
<cuba-app api-url="/app/rest/"></cuba-app>
Его необходимо добавить один раз в ваше приложение как можно раньше. Нельзя изменять свойства элемента динамически, а также удалять элемент после инициализации.
Для загрузки данных просто поместите элементы cuba-data в HTML и укажите требуемые атрибуты.
Загрузка Сущностей
Используйте cuba-entities для загрузки сущностей. Если указаны атрибуты entity-name и view, элемент загрузит список сущностей и передаст его для привязки данных в Polymer через свойство data:
<cuba-entities entity-name="sec$User" view="_local" data="{{users}}"></cuba-entities>
Теперь отобразить данные можно очень просто:
<template is="dom-repeat" items="[[users]]" as="user">
<div>[[user.login]]</div>
</template>
Использование предопределенных JPQL запросов
Составьте запрос, как описано здесь.
Используйте элемент cuba-query для получения результатов запроса. При необходимости в запрос можно передать параметры с помощью свойства params:
<cuba-query id="query"
auto="[[auto]]"
entity-name="sec$User"
query-name="usersByName"
data="{{users}}">
</cuba-query>
<template is="dom-repeat" items="[[users]]" as="user">
<div>[[user.login]]</div>
</template>
Вызов Сервиса
Зарегистрируйте сервис и его методы, как описано здесь. Используйте элемент cuba-service для вызова метода:
<cuba-service service-name="cuba_ServerInfoService"
method="getReleaseNumber"
data="{{releaseNumber}}"
handle-as="text"></cuba-service>
Release number: [[releaseNumber]]
Создание Сущности
С помощью элементов cuba-entity-form и cuba-service-form можно легко отправлять данные на backend.
В примере ниже мы связываем объект user, который нужно сохранить, со свойством entity.
<cuba-entity-form id="entityForm"
entity-name="sec$User"
entity="[[user]]"
on-cuba-form-response="_handleFormResponse"
on-cuba-form-error="_handleFormError">
<label>Login: <input type="text" name="login" value="{{user.login::input}}"></label>
<label>Name: <input type="text" name="login" value="{{user.name::input}}"></label>
<button on-tap="_submit">Submit</button>
</cuba-entity-form>
<paper-toast id="successToast">Entity created</paper-toast>
<paper-toast id="errorToast">Entity creation error</paper-toast>
_submit: function() {
this.$.entityForm.submit();
},
_handleFormResponse: function() {
this.user = getUserStub();
this.$.successToast.open();
},
_handleFormError: function() {
this.$.errorToast.open();
}
|
Необходимо разрешить анонимный доступ к REST API, если вы хотите использовать приведённые выше примеры без обязательного входа в систему. |
Ознакомьтесь с Polymer’s styling guide. Основное отличие от традиционного подхода состоит в способе описания глобальных стилей. Так как в элементах Polymer используется Shadow DOM, глобальные стили не работают внутри компонентов. Вместо этого необходимо использовать style-modules. Для описания общих стилей используйте файл shares-styles.html который импортируется во все компоненты приложения.
|
Экспериментальная технология! Ещё не все браузеры поддерживают технологии из списка ниже (к примеру, service workers пока не поддерживаются в Safari). |
В настоящее время мы рекомендуем вместе с Polymer использовать технологии Progressive Web Applications, такие как web app manifest 2, чтобы добиться native-like присутствия на домашнем экране пользователя. См. файл manifest.json в модуле клиента Polymer.
Существуют два основных подхода:
Больше информации о том, как настроить и использовать service workers, вы можете найти здесь.
Начиная с версии 6.9 платформы, Studio предоставляет возможность скаффолдинга Polymer клиентов на базе TypeScript. При создании модуля Polymer клиента вы можете выбрать пресет клиента polymer2-typesript. Ниже приведены его основные отличия от версии на базе JavaScript.
Классы компонентов хранятся в отдельных файлах *.ts
myapp-component.ts:
namespace myapp {
const {customElement} = Polymer.decorators;
@customElement('myapp-component')
class MyappComponent extends Polymer.Element {
}
}
myapp-component.html
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./shared-styles.html">
<dom-module id="myapp-component">
<template>
<!-- some html markup -->
</template>
<script src="myapp-component.js"></script>
</dom-module>
В процессе сборки есть дополнительный этап - компиляция TypeScript
См. секцию scripts в package.json
{
"scripts": {
"build": "npm run compile && polymer build",
"compile": "tsc",
"watch": "tsc -w"
}
}
Перед polymer build добавлена команда npm run compile, которая запускает компиляцию TypeScript (tsc).
|
Если вы хотите, чтобы изменения в коде классов компонентов подхватывались Studio для hot deploy, необходимо вручную выполнить команду npm run watch в каталоге modules/polymer-client. |
5.6.8.1. Создание компонентов Polymer на TypeScript
С декораторами TypeScript от Polymer создание классов компонентов стало удобнее, а код компактнее. Рассмотрим декораторы на следующем примере:
/// <reference path="../bower_components/cuba-app/cuba-app.d.ts" />
/// <reference path="../bower_components/app-layout/app-drawer/app-drawer.d.ts" />
/// <reference path="../bower_components/app-layout/app-drawer-layout/app-drawer-layout.d.ts" />
namespace myapp {
// Create shortcuts to decorators
const {customElement, property, observe, query} = Polymer.decorators;
@customElement('myapp-component')
class MyappComponent extends (Polymer.mixinBehaviors([CubaAppAwareBehavior, CubaLocalizeBehavior], Polymer.Element) as
new () => Polymer.Element & CubaAppAwareBehavior & CubaLocalizeBehavior) {
@property({type: Boolean})
enabled: boolean;
@property({type: String})
caption: string;
@query('#drawer')
drawer: AppDrawerElement;
@observe('app')
_init(app: cuba.CubaApp) {
...
}
@computed('enabled', 'caption')
get enabledCaption() {
...
}
}
}
Больше примеров вы можете найти в репозитории polymer-decorators на GitHub.
Proxy
Для работы через прокси может потребоваться соответствующая конфигурация bower и npm. Чтобы разрешить bower и npm работать через прокси, создайте следующие файлы в папке modules/polymer-client/:
.bowerrc
{
"proxy":"http://<user>:<password>@<host>:<port>",
"https-proxy":"http://<user>:<password>@<host>:<port>"
}
.npmrc
proxy=http://<user>:<password>@<host>:<port>
https-proxy=http://<user>:<password>@<host>:<port>
NPM install failed
При выполнении npm install на Windows иногда появляется известная проблема.
При сборке может возникнуть следующая ошибка:
npm ERR! code EPERM
npm ERR! errno -4048
npm ERR! syscall rename
npm ERR! Error: EPERM: operation not permitted,
Чтобы её обойти, можно попробовать запретить Windows Defender или другое антивирусное ПО, убедитьбся, что ваш проект не открыт в какой-либо IDE, и снова запустить сборку.
О появлении стабильного решения можно будет узнать из этого тикета.
В данном руководстве порталом называется клиентский блок, способный решать следующие задачи:
Конкретное приложение может содержать несколько портальных модулей, предназначенных для различных целей, например, в случае приложения, автоматизирующего бизнес такси, это может быть публичный веб-сайт для клиентов, интеграционный модуль мобильного приложения заказа такси, интеграционный модуль мобильного приложения водителей, и т.д.
Базовый проект cuba платформы содержит в своем составе модуль portal, который является заготовкой для создания порталов в проектах. Он предоставляет базовую функциональность клиентского блока для работы с Middleware. Кроме того, универсальный REST API, включенный в модуль portal в качестве зависимости, запускается по умолчанию.
Рассмотрим основные компоненты, предоставляемые платформой для построения портала.
Имеет дополнительный метод isAuthenticated(), возвращающий true, если данная сессия принадлежит неанонимному, т.е. явно зарегистрировавшемуся с логином и паролем, пользователю.
При первом обращении некоторого пользователя к порталу SecurityContextHandlerInterceptor создает для него (или привязывает уже имеющуюся) анонимную сессию, регистрируясь на Middleware с именем пользователя, указанным в свойстве приложения cuba.portal.anonymousUserLogin. Регистрация производится методом loginTrusted(), поэтому в блоке портала необходимо установить также свойство cuba.trustedClientPassword. Таким образом, любой анонимный пользователь портала может работать с сервисами Middleware с правами пользователя cuba.portal.anonymousUserLogin.
Если портал содержит страницу регистрации пользователя с именем и паролем, то после выполнения Connection.login() при обработке запросов SecurityContextHandlerInterceptor устанавливает в потоке выполнения пользовательскую сессию явно зарегистрированного пользователя, и работа с Middleware происходит от его имени.
Универсальный REST API предоставляет следующую функциональность:
REST API использует протокол OAuth2 для аутентификации и поддерживает анонимный доступ.
Подробная документация по REST API доступна по следующему адресу: http://files.cuba-platform.com/swagger/6.10.
Раздел Использование REST API сборника рецептов содержит большое количество примеров, демонстрирующих возможности REST API в действии. В разделах ниже приведено формальное описание некоторых особенностей и конфигурационных параметров.
5.8.1. Настройка предопределенных JPQL запросов
В приложении на CUBA предопределенные JPQL запросы должны быть объявлены в файлах, определенных свойством приложения cuba.rest.queriesConfig. Свойство должно быть определено в модуле web или portal (например, в файле web-app.properties):
cuba.rest.queriesConfig = +com/company/myapp/rest-queries.xml
Файл rest-queries.xml должен находиться в главном пакете модуля web или portal (например, com.company.myapp). Его содержимое определяется схемой rest-queries.xsd, например:
<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
<query name="carByVin" entity="sample$Car" view="carEdit">
<jpql><![CDATA[select c from sample$Car c where c.vin = :vin]]></jpql>
<params>
<param name="vin" type="java.lang.String"/>
</params>
</query>
<query name="allColours" entity="sample$Colour" view="_local">
<jpql><![CDATA[select u from sample$Colour u order by u.name]]></jpql>
</query>
<query name="carsByIds" entity="sample$Car" view="carEdit" cacheable="true">
<jpql><![CDATA[select c from sample$Car c where c.id in :ids]]></jpql>
<params>
<param name="ids" type="java.util.UUID[]"/>
</params>
</query>
<query name="myOrders" entity="sample$Order" view="orderBrowse">
<jpql><![CDATA[select o from sample$Order o where o.createdBy = :session$userLogin]]></jpql>
</query>
</queries>
Пример конфигурирования и исполнения запроса можно увидеть в разделе Выполнение JPQL-запроса (GET) и Выполнение JPQL-запроса (POST).
Платформа также предоставляет встроенный запрос all для получения списка всех экземпляров некоторой сущности. Он может быть использован совместно с /count чтобы получить общее количество экземпляров сущности, например:
http://localhost:8080/app/rest/v2/queries/sales$Order/all/count
Атрибут cacheable элемента query включает кэширование данного запроса.
Запрос может содержать предопределенные параметры, которые принимают значения идентификатора и логина текущего пользователя: session$userId и session$userLogin. Их не нужно объявлять в элементе params (см. пример выше).
5.8.2. Настройка сервисов среднего слоя
Список методов сервисов, доступных для вызова через REST API, должен быть объявлен в приложении в конфигурационных файлах, заданных свойством приложения cuba.rest.servicesConfig. Свойство должно быть определено в модуле web или portal (например, в файле web-app.properties):
cuba.rest.servicesConfig = +com/company/myapp/rest-services.xml
Файл rest-services.xml должен находиться в главном пакете модуля web или portal (например, com.company.myapp). Его содержимое определяется схемой rest-services-v2.xsd, например:
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
<service name="myapp_SomeService">
<method name="sum">
<param name="number1"/>
<param name="number2"/>
</method>
<method name="emptyMethod"/>
<method name="overloadedMethod">
<param name="intParam" type="int"/>
</method>
<method name="overloadedMethod">
<param name="stringParam" type="java.lang.String"/>
</method>
</service>
</services>
Типы параметров метода могут быть опущены, если сервис не содержит перегруженного метода с тем же количеством аргументов. Иначе типы параметров обязательны к указанию.
Пример конфигурирования и вызова сервиса можно увидеть в разделе Вызов метода сервиса (GET).
Если необходимо иметь возможность вызова метода сервиса без аутентификации даже при отключенном анонимном доступе к REST API, то можно пометить метод сервиса атрибутом anonymousAllowed="true" в конфигурационном файле:
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
<service name="myapp_SomeService">
<method name="sum" anonymousAllowed="true">
<param name="number1"/>
<param name="number2"/>
</method>
</service>
</services>
5.8.3. Версионирование модели данных
REST API поддерживает работу с различными версиями модели данных. Это может быть полезно в случае, например, когда имя атрибута сущности было изменено, но клиент REST API не знает об этом изменении и ожидает, что атрибут все еще имеет старое имя.
Для подобных случаев вы можете описать правила трасформации JSON сущностей. Если клиентское приложение посылает версию модели данных в параметре запроса, то JSON ответа, сформированном REST API, или тело запроса к REST API будут трансформированы согласно правилам, объявленным для конкретной версии модели данных.
Правила трансформации JSON должны быть объявлены в файлах, зарегистрированных в свойстве приложения cuba.rest.jsonTransformationConfig для модуля web или portal (например, в файле web-app.properties):
cuba.rest.jsonTransformationConfig = +com/company/myapp/rest-json-transformations.xml
Файл rest-json-transformations.xml должен быть расположен в модуле web или portal (например, в пакете com.company.myapp). Содержимое файла определяется схемой rest-json-transformations.xsd. Пример файла:
<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
<transformation modelVersion="1.0" oldEntityName="sales$OldOrder" currentEntityName="sales$NewOrder">
<renameAttribute oldName="oldNumber" currentName="number"/>
<renameAttribute oldName="date" currentName="deliveryDate"/>
<toVersion>
<removeAttribute name="discount"/>
</toVersion>
</transformation>
<transformation modelVersion="1.0" currentEntityName="sales$Contractor">
<renameAttribute oldName="summary" currentName="total"/>
<renameAttribute oldName="familyName" currentName="lastName"/>
<fromVersion>
<removeAttribute name="city"/>
<removeAttribute name="country"/>
</fromVersion>
<toVersion>
<removeAttribute name="phone"/>
</toVersion>
</transformation>
<transformation modelVersion="1.1" currentEntityName="sales$NewOrder">
<renameAttribute oldName="date" currentName="deliveryDate"/>
</transformation>
</transformations>
Стандартные трансформеры, определяемые в конфигурационном файле, могут осуществлять следующие типы трансформаций JSON:
Трансформация JSON работает для следующих адресов REST API:
Трансформации JSON применяются, если запрос к REST API содержит параметр modelVersion со значением версии модели данных в URL.
Раздел Примеры версионирования модели данных содержит примеры настройки версионирования модели данных и использования его из клиентских приложений.
По умолчанию все кросс-доменные запросы к REST API разрешены. Для ограничения списка разрешенных хостов укажите список хостов через запятую в свойстве приложения cuba.rest.allowedOrigins.
По умолчанию анонимный доступ к REST API запрещен. Для его включения установите свойство приложения cuba.rest.anonymousEnabled в true. Запрос считается анонимным, если в нем отсутствует заголовок Authentication. В этом случае SecurityContext будет содержать анонимную сессию.
Чтобы определить разрешения для анонимного доступа необходимо задать набор ролей для пользователя, имя которого хранится в свойстве приложения cuba.anonymousLogin.
5.8.6. Прочие настройки REST API
cuba.rest.client.id - определяет id клиента REST API по умолчанию.
cuba.rest.client.secret - определяет пароль клиента REST API по умолчанию.
cuba.rest.client.tokenExpirationTimeSec - определяет время жизни access токена в секундах для клиента по умолчанию.
cuba.rest.client.refreshTokenExpirationTimeSec - определяет время жизни refresh токена в секундах для клиента по умолчанию.
cuba.rest.client.authorizedGrantTypes - определяет список типов авторизации (grant type), поддерживаемых клиентом по умолчанию. Для отключения поддержки refresh-токенов, удалите элемент refresh_token из значения свойства.
cuba.rest.maxUploadSize - определяет максимальный размер файла, который может быть загружен с помощью REST API.
cuba.rest.reuseRefreshToken - определяет должен ли refresh-токен быть повторно использован.
cuba.rest.requiresSecurityToken - указывает, что в JSON сущности должен пересылаться дополнительный системный атрибут. Подробнее см. Ограничения для атрибутов-коллекций.
cuba.rest.tokenMaskingEnabled - указывает, должны ли значения токенов REST API быть маскированы в логах приложения.
5.8.7. Собственные контроллеры, защищенные OAuth2
Если вам необходимо создать свой REST контроллер, защищенный с помощью OAuth2, сделайте следующее:
package com.company.test.portal.myapi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.company.test.services.SomeService;
@RestController
@RequestMapping("/myapi")
public class MyController {
@Inject
protected SomeService someService;
@GetMapping("/dosmth")
public String doSmth() {
return someService.getResult();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd">
<!-- Define a base package for your controllers-->
<context:component-scan base-package="com.company.test.portal.myapi"/>
<security:http pattern="/rest/myapi/**"
create-session="stateless"
entry-point-ref="oauthAuthenticationEntryPoint"
xmlns="http://www.springframework.org/schema/security">
<!-- Specify one or more protected URL patterns-->
<intercept-url pattern="/rest/myapi/**" access="isAuthenticated()"/>
<anonymous enabled="false"/>
<csrf disabled="true"/>
<cors configuration-source-ref="cuba_RestCorsSource"/>
<custom-filter ref="resourceFilter" before="PRE_AUTH_FILTER"/>
<custom-filter ref="cuba_AnonymousAuthenticationFilter" after="PRE_AUTH_FILTER"/>
</security:http>
</beans>
cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml
|
Адреса для доступа к методам кастомных контроллеров НЕ ДОЛЖНЫ начинаться с /rest/v2. |
5.8.8. Ограничения для атрибутов-коллекций
Рассмотрим следующую ситуацию:
Если в вашем приложении подобная ситуация возможна, то избежать потери данных можно путем пересылки в JSON специального системного атрибута. Данный атрибут имеет имя __securityToken и автоматически включается в результирующий JSON если свойство приложения cuba.rest.requiresSecurityToken установлено в true. В этом случае в обязанности вашего REST-клиента входит передача этого атрибута обратно при сохранении сущности.
Пример JSON сущности с включенным security token:
{
"id": "fa430b56-ceb2-150f-6a85-12c691908bd1",
"number": "OR-000001",
"items": [
{
"id": "82e6e6d2-be97-c81c-c58d-5e2760ae095a",
"description": "Item 1"
},
{
"id": "988a8cb5-d61a-e493-c401-f717dd9a2d66",
"description": "Item 2"
}
],
"__securityToken": "0NXc6bQh+vZuXE4Fsk4mJX4QnhS3lOBfxzUniltchpxPfi1rZ5htEmekfV60sbEuWUykbDoY+rCxdhzORaYQNQ=="
}
Атрибут __securityToken содержит закодированные идентификаторы отфильтрованных экземпляров, так что фреймворк всегда может восстановить необходимую информацию независимо от изменений в настройках ограничений.
5.8.9. Персистентное хранилище токенов
По умолчанию OAuth токены хранятся только в памяти. Для того, чтобы параллельно хранить их базе данных установите свойство cuba.rest.storeTokensInDb в true. Значение свойства хранится в базе данных, следовательно редактировать его можно из экрана Администрирование > Свойства приложения.
Истекшие токены должны периодически удаляться из базы данных. Выражение cron, определяющее расписание процедуры удаления, определено свойством приложения cuba.rest.deleteExpiredTokensCron.
5.8.10. Swagger документация по проекту
Документация по обобщенному REST API доступна по адресу http://files.cuba-platform.com/swagger/6.10.
Любое запущенное приложение на CUBA также экспортирует документацию конкретно для данного проекта, сгенерированную в соответствии со спецификацией Swagger версии 2.0.
Генерируемая документация доступна по следующим адресам:
Пример:
http://localhost:8080/app/rest/v2/docs/swagger.yaml
http://localhost:8080/app/rest/v2/docs/swaggerDetailed.yaml
Проектная документация может использоваться для визуализации, тестирования, или генерации клиентского кода для REST API. См. следующие инструменты: Swagger UI, Swagger Inspector, Postman, Swagger Codegen.
Документация включает в себя:
Также для всех параметров и ответов CRUD доступна подробная модель, к примеру:
В данной главе рассматриваются различные опциональные возможности, предоставляемые платформой.
Динамические атрибуты - это дополнительные поля сущности, которые можно добавлять без изменения схемы БД и перезагрузки приложения. Механизм динамических атрибутов предназначен для описания новых свойств сущностей на этапе настройки и эксплуатации системы.
Динамические атрибуты CUBA являются реализацией концепции Entity-Attribute-Value.
Рисунок 27. Диаграмма классов механизма динамических атрибутов
Например, имеется сущность типа Автомобиль. Для нее можно определить две категории: Грузовой и Пассажирский. При этом категория Грузовой будет содержать атрибуты Грузоподъемность и Вид кузова, а категория Пассажирский - атрибуты Количество мест и Наличие детского сидения.
Экземпляр сущности может иметь атрибуты одновременно из всех категорий, связанных с этим типом сущности. Если необходимо, чтобы некоторый экземпляр сущности принадлежал только одной категории с соответствующим набором атрибутов (Автомобиль может быть либо Грузовым, либо Пассажирским), класс сущности должен реализовывать интерфейс Categorized. В этом случае экземпляр сущности будет содержать ссылку на категорию и динамические атрибуты только выбранной категории.
Загрузка и сохранение динамических атрибутов осуществляется в DataManager. Для указания того, что динамические атрибуты должны быть загружены вместе с экземплярами сущностей, используется метод LoadContext.setLoadDynamicAttributes(). По умолчанию динамические атрибуты не загружаются. В то же время DataManager всегда сохраняет динамические атрибуты, содержащиеся в экземплярах сущностей, переданных в commit().
Доступ к значениям динамических атрибутов может быть осуществлен через методы getValue() / setValue() любой персистентной сущности, унаследованной от BaseGenericIdEntity. В эти методы необходимо передавать код атрибута с префиксом +, например:
LoadContext lc = new LoadContext(Car.class).setId(id);
lc.setLoadDynamicAttributes(true);
Entity entity = dataManager.load(lc);
Double capacity = entity.getValue("+loadCapacity");
entity.setValue("+loadCapacity", capacity + 10);
dataManager.commit(entity);
На самом деле, прямой доступ к значениям динамических атрибутов в коде приложения нужен крайне редко. Любой динамический атрибут может быть автоматически отображен в любом компоненте Table или FieldGroup, связанном с источником данных, содержащим сущность, для которой данный атрибут был создан. Экран редактирования атрибута позволяет указать, в каких экранах и компонентах отображать атрибут.
Разрешения пользователей на доступ к динамическим атрибутам назначаются в редакторе ролей так же как и для обычных атрибутов. Динамические атрибуты отображаются с префиксом +.
5.9.1.1. Управление динамическими атрибутами
Управление категориями и описаниями атрибутов осуществляется с помощью специальных экранов, доступных через меню Administration → Dynamic Attributes.
Рисунок 28. Экран списка категорий
Редактор категорий позволяет создать категорию для выбранного типа сущности и добавить в нее набор динамических атрибутов. Для категории обязательно указывается имя и соответствующий тип сущности. Флажок Default указывает, что данная категория будет автоматически выбрана для нового экземпляра сущности, реализующей интерфейс Categorized.
Рисунок 29. Экран редактирования категории
Секция Name localization отображается, если приложение поддерживает более одного языка, и позволяет задать локализованное значение имени категории для каждой доступной локали.
Рисунок 30. Локализация имени категории
Редактор динамического атрибута позволяет задать имя, системный код, тип значения и значение атрибута по умолчанию.
Рисунок 31. Редактор динамического атрибута
Для всех типов значения, кроме Boolean, доступно поле Width, позволяющее задать ширину поля в FieldGroup в пикселах или процентах. Если поле Width не заполнено, его значение по умолчанию равно 100%.
Для всех типов значения, кроме Boolean и Enumeration, также доступен чекбокс Is collection. Он позволяет создавать динамические атрибуты выбранного типа со множеством значений.
Для всех типов значения поддерживается локализация имени атрибута:
Рисунок 32. Локализация динамического атрибута
Если выбран тип значения Enumeration, множество значений перечисления задаётся в поле Enumeration в редакторе списка.
Рисунок 33. Редактор динамического атрибута для типа значений Enumeration
Каждое значение перечисления также может быть локализовано на языки, доступные в приложении:
Рисунок 34. Локализация динамического атрибута для типа значений Enumeration
Динамический атрибут также имеет настройки видимости, описывающие, на каких экранах его нужно отображать. По умолчанию атрибут не отображается нигде.
Рисунок 35. Настройки видимости динамического атрибута
Кроме экрана можно также указать компонент, в котором атрибут должен появляться (например, для экранов, где несколько компонентов FieldGroup показывают поля одной и той же сущности).
Если атрибут отмечен как видимый на каком-либо экране, он автоматически отобразится во всех группах полей и таблицах, отображающих объекты данного типа в данном экране.
Доступ к динамическим атрибутам также может быть ограничен через настройки в ролях пользователей. Настройки осуществляются так же, как для обычных атрибутов.
Для того чтобы изменения в атрибутах и настройках видимости вступили в силу, необходимо нажать кнопку Применить изменения на экране со списком категорий. Изменения также можно применить через Administration → JMX Console, вызвав метод clearDynamicAttributesCache() JMX бина app-core.cuba:type=CachingFacade.
Ниже изображен динамический атрибут, добавленный в экран автоматически путем задания настроек отображения атрибута:
Динамические атрибуты можно добавить в экран вручную. Для этого необходимо выполнить следующее:
<dsContext>
<datasource id="carDs" class="com.company.sample.entity.Car" view="_local" loadDynamicAttributes="true"/>
</dsContext>
<textField id="numberOfSeats" datasource="carDs" property="+numberOfSeats"/>
5.9.1.2. Категоризируемые сущности
Если сущность реализует интерфейс com.haulmont.cuba.core.entity.Categorized, то для работы с ее динамическими атрибутами можно использовать компонент com.haulmont.cuba.gui.components.RuntimePropertiesFrame. Этот компонент позволяет пользователю выбрать для экземпляра сущности некоторую категорию и указать значения динамических атрибутов этой категории.
Для использования RuntimePropertiesFrame в экране редактирования необходимо выполнить следующее:
Например:
<dsContext>
<datasource id="carDs"
class="com.company.sample.entity.Car"
view="carEdit"/>
<runtimePropsDatasource id="runtimePropsDs"
mainDs="carDs"/>
<collectionDatasource id="categories"
class="com.haulmont.cuba.core.entity.Category"
view="_local">
<query>
select c from sys$Category c where c.entityType='sample$Car'
</query>
</collectionDatasource>
</dsContext>
<runtimeProperties id="runtimePropsFrame"
runtimeDs="runtimePropsDs"
categoriesDs="categories"/>
Платформа предоставляет средства отправки сообщений электронной почты со следующими возможностями:
Пример использования данного механизма рассмотрен в рецепте Отправка email.
Для отправки email на Middleware следует использовать бин EmailerAPI, на клиентском уровне - сервис EmailService.
Рассмотрим основные методы этих компонентов:
Сообщение может быть передано как в виде набора параметров (список адресатов через запятую, тема, содержимое, массив вложений), так и в виде специального объекта EmailInfo, инкапсулирующего всю эту информацию, плюс позволяющего явно задать адрес отправителя и сформировать тело письма по шаблону FreeMarker.
При синхронной отправке может быть сгенерировано исключение EmailException, несущее в себе информацию о том, по каким адресам отправка не удалась, и соответствующие им сообщения об ошибках.
В процессе работы метода для каждого адресата в базе данных создается экземпляр сущности SendingMessage, который сначала получает статус SendingStatus.SENDING, а после успешной отправки - SendingStatus.SENT. В случае ошибки отправки статус сообщения меняется на SendingStatus.NOTSENT.
Объект EmailAttachment - обёртка, хранящая вложение в виде массива байт (поле data), имя файла (поле name), и при необходимости, уникальный для данного сообщения идентификатор вложения (необязательное, но полезное поле contentId).
Идентификатор вложения может быть использован для вставки в сообщение изображений следующим образом:при создании EmailAttachment задаётся уникальный contentId, например, myPic. В теле письма для вставки вложения необходимо в качестве пути использовать запись вида: cid:myPic. Т.е. для вставки изображения нужно указать следующий элемент HTML:
<img src="cid:myPic"/>
5.9.2.3. Настройка параметров отправки email
Параметры отправки email могут быть настроены с помощью перечисленных ниже свойств приложения. Все они являются параметрами времени выполнения и хранятся в базе данных, однако могут быть переопределены для конкретного блока Middleware в его файле app.properties.
Все параметры отправки email доступны через конфигурационный интерфейс EmailerConfig.
Значение по умолчанию: DoNotReply@localhost
Значение по умолчанию: test.host
Значение по умолчанию: 25
Значение по умолчанию: false
Значение по умолчанию: false
Значение по умолчанию: false
Значение по умолчанию: 2
Значение по умолчанию: 100
Значение по умолчанию: 10
Значение по умолчанию: 120
Значение по умолчанию: false
Значение по умолчанию: admin@localhost
Значение по умолчанию: admin
В шаблонах используется синтаксис Groovy SimpleTemplateEngine, что позволяет использовать блоки кода Groovy прямо в тексте шаблона.
Пример файла шаблона:
<html>
<body>
<p>${timestamp}</p>
<p>${toHtml(errorMessage)}</p>
<p>${toHtml(stacktrace)}</p>
</body>
</html>
Пример файла шаблона:
[${systemId}] [${userLogin}] Exception Report
Чтобы использовать свойства из JavaMail API, их необходимо добавить в файл app.properties модуля core. Свойства, начинающиеся с mail.*, используются при создании объекта javax.mail.Session.
Просмотреть текущие значения параметров, а также отправить тестовое сообщение, можно с помощью JMX-бина app-core.cuba:type=Emailer.
Инспектор сущностей позволяет работать с любыми объектами предметной области без создания специфических экранов. Инспектор динамически генерирует экраны просмотра списка и редактирования экземпляра выбранной сущности.
Это дает возможность администратору системы просматривать и редактировать данные, которые недоступны в стандартных экранах в силу их дизайна, а на этапе прототипирования создать только модель данных и пункты главного меню, ссылающиеся на инспектор сущностей.
Точкой входа в инспектор является экран com/haulmont/cuba/gui/app/core/entityinspector/entity-inspector-browse.xml.
Если в экран передан параметр entity типа String с именем сущности, то инспектор отобразит список экземпляров этой сущности с возможностью фильтрации, выбора и редактирования экземпляров. Параметр может быть указан при регистрации экрана в screens.xml, например:
screens.xml
<screen id="sales$Product.lookup"
template="/com/haulmont/cuba/gui/app/core/entityinspector/entity-inspector-browse.xml">
<param name="entity"
value="sales$Product"/>
</screen>
menu.xml
<item id="sales$Product.lookup"/>
Идентификатор экрана вида {имя_сущности}.lookup дает возможность использовать этот экран компонентам PickerField и LookupPickerField в стандартном действии PickerField.LookupAction.
В общем случае данный экран можно вызывать без передачи параметров, тогда в его верхней части отображается поле для выбора сущности. В компоненте cuba экран инспектора зарегистрирован с идентификатором entityInspector.browse, поэтому для его вызова достаточно наличия пункта меню:
<item id="entityInspector.browse"/>
5.9.4. Журнал изменений сущностей
Механизм журналирования предназначен для отслеживания изменений атрибутов произвольных сущностей в процессе работы приложения. Измененные значения сохраняются в специальной таблице базы данных, список изменений для конкретного экземпляра сущности может быть отображен в пользовательском интерфейсе.
Данный механизм перехватывает сохранение сущностей в БД на уровне Entity Listeners, т.е. гарантированно отслеживаются все изменения, проходящие через персистентный контекст EntityManager. Непосредственное изменение сущностей в базе данных с помощью SQL, в том числе изнутри системы через NativeQuery и QueryRunner, в журнал не попадает.
Измененные экземпляры сущностей перед сохранением в БД отправляются в методы registerCreate(), registerModify(), registerDelete() бина EntityLogAPI. Параметр auto этих методов позволяет отделить автоматическое журналирование посредством Entity Listeners от ручного вызова этих же методов в прикладном коде. При вызове из Entity Listeners в параметре auto передается true.
Журнал содержит информацию о том, кто и когда изменил данный экземпляр, а также новые значения измененных атрибутов. Записи журнала сохраняются в таблице SEC_ENTITY_LOG базы данных, соответствующей сущности EntityLogItem. Измененные значения атрибутов хранятся в этой же таблице в колонке CHANGES, а при чтении на Middleware преобразуются в экземпляры сущности EntityLogAttr.
5.9.4.1. Настройка журналирования
Простейший способ настройки аудита - воспользоваться экраном приложения Administration > Entity Log > Setup.
Вы также можете настроить аудит путем внесения информации напрямую в БД, если необходимо включить конфигурацию в скрипты инициализации базы данных.
Аудит настраивается при помощи сущностей LoggedEntity и LoggedAttribute (соответствующих таблицам SEC_LOGGED_ENTITY и SEC_LOGGED_ATTR).
LoggedEntity описывает тип сущности, изменения которой необходимо журналировать. Атрибуты LoggedEntity:
LoggedAttribute описывает журналируемый атрибут сущности и содержит ссылку на LoggedEntity и имя атрибута.
Для настройки журналирования некоторой сущности достаточно внести соответствующие записи в таблицы SEC_LOGGED_ENTITY и SEC_LOGGED_ATTR. Например, для ведения журнала изменений атрибутов name и grade сущности Customer, необходимо выполнить:
insert into SEC_LOGGED_ENTITY (ID, CREATE_TS, CREATED_BY, NAME, AUTO, MANUAL)
values ('25eeb644-e609-11e1-9ada-3860770d7eaf', now(), 'admin', 'sales$Customer', true, true);
insert into SEC_LOGGED_ATTR (ID, CREATE_TS, CREATED_BY, ENTITY_ID, NAME)
values (newid(), now(), 'admin', '25eeb644-e609-11e1-9ada-3860770d7eaf', 'name');
insert into SEC_LOGGED_ATTR (ID, CREATE_TS, CREATED_BY, ENTITY_ID, NAME)
values (newid(), now(), 'admin', '25eeb644-e609-11e1-9ada-3860770d7eaf', 'grade');
Механизм журналирования по умолчанию активен. Если вы хотите его отключить, необходимо установить в false атрибут Enabled JMX-бина app-core.cuba:type=EntityLog и вызвать его операцию invalidateCache(). В качестве альтернативы можно установить в false значение свойства приложения cuba.entityLog.enabled и рестартовать сервер.
Для просмотра журнала изменений некоторого экземпляра сущности достаточно обычным способом загрузить в источники данных экрана коллекцию экземпляров EntityLogItem и ассоциированных с ними EntityLogAttr, и создать визуальные компоненты, связанные с этими источниками. Например:
<dsContext>
<datasource id="customerDs"
class="com.sample.sales.entity.Customer"
view="customerEdit"/>
<collectionDatasource id="logDs"
class="com.haulmont.cuba.security.entity.EntityLogItem"
view="logView">
<query>
select i from sec$EntityLog i
where i.entityRef.entityId = :ds$customerDs order by i.eventTs
</query>
<collectionDatasource id="logAttrDs"
property="attributes"/>
</collectionDatasource>
</dsContext>
<layout>
...
<split orientation="vertical" width="100%" height="100%">
<table id="logTable" width="100%" height="100%">
<columns>
<column id="eventTs"/>
<column id="user.login"/>
<column id="type"/>
</columns>
<rows datasource="logDs"/>
</table>
<table id="logAttrTable" width="100%" height="100%">
<columns>
<column id="name"/>
<column id="value"/>
</columns>
<rows datasource="logAttrDs"/>
</table>
</split>
...
</layout>
Для отображения локализованных значений журналируемых атрибутов эти атрибуты должны содержать аннотацию @LocalizedValue. При ее наличии механизм журналирования заполняет поле EntityLogAttr.messagesPack, и таблица, отображающая значения атрибутов из примера выше может использовать колонку locValue вместо value:
<table id="logAttrTable" width="100%" height="100%">
<columns>
<column id="name"/>
<column id="locValue"/>
</columns>
<rows datasource="logAttrDs"/>
</table>
Механизм сохранения снимков сущностей, так же как и журнал изменений, предназначен для отслеживания изменений данных в процессе работы приложения. Его отличительными особенностями являются:
Для сохранения снимка некоторого графа сущностей достаточно вызвать метод EntitySnapshotService.createSnapshot() и передать ему основную сущность графа и представление, описывающее граф. Снимок создается по загруженной сущности, никаких обращений к базе данных не производится, поэтому снимок в результате содержит не больше полей, чем представление, с которым была загружена основная сущность.
Граф Java объектов преобразуется в XML и сохраняется в базе данных вместе со ссылкой на основную сущность в таблице SYS_ENTITY_SNAPSHOT, соответствующей сущности EntitySnapshot.
Как правило, снимки требуется сохранять после коммита экрана редактирования. Для этого можно переопределить метод postCommit() контроллера экрана, например:
public class CustomerEditor extends AbstractEditor<Customer> {
@Inject
protected Datasource<Customer> customerDs;
@Inject
protected EntitySnapshotService entitySnapshotService;
...
@Override
protected boolean postCommit(boolean committed, boolean close) {
if (committed) {
entitySnapshotService.createSnapshot(customerDs.getItem(), customerDs.getView());
}
return super.postCommit(committed, close);
}
}
Для отображения сохраненных для некоторой сущности снимков можно использовать фрейм com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml, например:
<frame id="diffFrame"
src="/com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml"
width="100%"
height="100%"/>
В контроллере экрана редактирования необходимо вызвать загрузку снимков во фрейм:
public class CustomerEditor extends AbstractEditor<Customer> {
@Inject
private EntityStates entityStates;
@Inject
protected EntityDiffViewer diffFrame;
...
@Override
protected void postInit() {
if (!entityStates.isNew(getItem())) {
diffFrame.loadVersions(getItem());
}
}
}
Фрейм diff-view.xml отображает список сохраненных для данной сущности снимков с возможностью их сравнения. Для каждого снимка указывается пользователь, дата и время сохранения. При выборе из списка некоторого снимка сущности в таблице сравнения показываются изменения данных по сравнению с предыдущим снимком. В первом снимке измененными считаются все атрибуты. Если выбрано два снимка, то в таблицу выводится результат их сравнения.
В таблице сравнения отображаются имена атрибутов и их новые значения, при выборе строки показывается детальная информация по изменениям атрибута в двух снимках. Ссылочные поля выводятся в соответствии с их шаблоном @NamePattern. При сравнении коллекций добавленные и удаленные элементы выделяются цветом (зеленый, красный), а элементы с измененными атрибутами остаются без выделения. Изменение позиций элементов не учитывается.
Механизм статистики сущностей предоставляет данные о текущем количестве экземпляров сущностей в базе данных. Эти данные используются для автоматического принятия решений о выборе способа поиска связанных сущностей и ограничении размера выборок в экранах пользовательского интерфейса.
Статистика хранится в таблице SYS_ENTITY_STATISTICS, соответствующей сущности EntityStatistics. Заполнить статистику можно как вручную, внося соответствующие записи в таблицу, так и автоматически с помощью метода refreshStatistics() JMX-бина PersistenceManagerMBean. При указании в качестве параметра имени сущности статистика будет собрана только для данной сущности, в противном случае - для всех. Сбор статистики может занять значительное время и вызвать нежелательную нагрузку на БД, поэтому выполнять его нужно либо вручную, либо назначенным заданием в подходящее время.
Программный доступ к статистике осуществляется с помощью интерфейса PersistenceManagerAPI на Middleware и PersistenceManagerService на клиентском уровне. Статистика кэшируется в памяти, поэтому если изменения статистики вносятся напрямую в базу данных, для вступления их в силу необходимо перезапустить сервер или вызвать метод PersistenceManagerMBean.flushStatisticsCache().
Рассмотрим атрибуты EntityStatistics и их влияние на поведение системы.
Например, компонент Filter устанавливает это число в поле Показывать N строк.
Данный параметр играет роль при отображении списков сущностей в компонентах типа LookupField и LookupPickerField, а также в таблицах без универсального фильтра, то есть когда на связанный источник данных не налагается ограничений методом CollectionDatasource.setMaxResults(). В этом случае сам источник данных ограничивает количество извлекаемых экземпляров значением maxFetchUI.
В частности, этот параметр принимается во внимание компонентом Filter при выборе параметров фильтрации: до достижения порога используется компонент LookupField, при превышении порога - компонент PickerField. Поэтому, если необходимо заставить фильтр отображать выбор параметра некоторого типа через экран выбора, достаточно внести запись статистики для этой сущности со значением lookupScreenThreshold меньшим, чем instanceCount.
JMX-бин PersistenceManagerMBean в атрибутах DefaultFetchUI, DefaultMaxFetchUI, DefaultLookupScreenThreshold позволяет задать значения вышеперечисленных параметров по умолчанию. В результате, если для некоторой сущности статистика отсутствует (что является обычной ситуацией), будет использоваться соответствующий параметр по умолчанию.
Кроме того, JMX-бин PersistenceManagerMBean позволяет ввести данные статистики для конкретной сущности с помощью операции enterStatistics(). Например, для того, чтобы для сущности sales$Customer установить размер страницы данных по умолчанию в 1000, а максимальное количество извлекаемых экземпляров в компонентах LookupField в 30000, следует вызвать операцию enterStatistics() со следующими параметрами:
entityName: sales$Customer
fetchUI: 1000
maxFetchUI: 30000
Другой пример: у вас есть фильтр с условием по сущности Customer, и вы хотите использовать экран выбора вместо выпадающего списка при выборе параметра типа Customer. Тогда вызовите метод enterStatistics() со следующими параметрами:
entityName: sales$Customer
instanceCount: 2
lookupScreenThreshold: 1
В данном случае реальное число записей Customer в базе данных игнорируется, и явно устанавливается превышение порога.
5.9.7. Экспорт и импорт сущностей в JSON
Платформа предоставляет API для экспортирования и импортирования графов сущностей в формате JSON. Он доступен на Middleware в виде интерфейса EntityImportExportAPI, и на клиентском уровне в виде сервиса EntityImportExportService. Эти интерфейсы имеют одинаковый набор методов, описанный ниже. Реализация экспорта/импорта делегирует выполнение интерфейсу EntitySerializationAPI, который при необходимости может быть использован напрямую.
@Inject
private EntityImportExportService entityImportExportService;
@Inject
private GroupDatasource<Customer, UUID> customersDs;
...
String jsonFromDs = entityImportExportService.exportEntitiesToJSON(customersDs.getItems());
@Inject
private EntityImportExportService entityImportExportService;
@Inject
private GroupDatasource<Customer, UUID> customersDs;
@Inject
private Metadata metadata;
@Inject
private DataManager dataManager;
...
byte[] array = entityImportExportService.exportEntitiesToZIP(customersDs.getItems());
FileDescriptor descriptor = metadata.create(FileDescriptor.class);
descriptor.setName("customersDs.zip");
descriptor.setExtension("zip");
descriptor.setSize((long) array.length);
descriptor.setCreateDate(new Date());
try {
fileLoader.saveStream(descriptor, () -> new ByteArrayInputStream(array));
} catch (FileStorageException e) {
throw new RuntimeException(e);
}
dataManager.commit(descriptor);
@Inject
private EntityImportExportService entityImportExportService;
@Inject
private FileLoader fileLoader;
private FileDescriptor descriptor;
...
EntityImportView view = new EntityImportView(Customer.class);
view.addLocalProperties();
try {
byte[] array = IOUtils.toByteArray(fileLoader.openStream(descriptor));
Collection<Entity> collection = entityImportExportService.importEntitiesFromZIP(array, view);
} catch (FileStorageException e) {
throw new RuntimeException(e);
}
Хранилище файлов обеспечивает загрузку, хранение и выгрузку произвольных файлов, ассоциированных с сущностями системы. Стандартная реализация сохраняет файлы вне основной базы данных, в специальной структуре файловой системы.
Механизм работы с файлами состоит из следующих частей:
|
Передача файлов между пользовательским компьютером и хранилищем в обе стороны производится только путем копирования данных между потоками ввода-вывода. Ни на каком уровне приложения файл не оказывается целиком в памяти, поэтому возможна передача файлов практически любых размеров. |
Для загрузки файлов с компьютера пользователя в хранилище следует использовать компоненты FileUpload и FileMultiUpload. Примеры использования приведены в описании компонентов.
Компонент FileUpload также можно использовать в готовом диалоговом окне FileUploadDialog, позволяющем загружать файлы в промежуточное хранилище.
Промежуточное хранилище клиентского уровня FileUploadingAPI для хранения временных файлов использует каталог, заданный свойством приложения cuba.tempDir. В случае сбоев в нем могут оставаться временные файлы, для удаления которых служит метод clearTempDirectory() бина cuba_FileUploading. Этот метод периодически вызывается шедулером, объявленным в файле cuba-web-spring.xml.
Для выгрузки файлов на клиентском уровне следует использовать интерфейс ExportDisplay, получив ссылку на него вызовом статического метода AppConfig.createExportDisplay(), либо инжекцией в класс контроллера. Например:
AppConfig.createExportDisplay(this).show(fileDescriptor);
Метод show() может принимать дополнительный параметр типа ExportFormat, в котором можно задать тип содержимого и расширение имени файла. Если формат не передан, расширение берется из FileDescriptor, а типом содержимого принимается application/octet-stream.
От расширения имени файла зависит, будет ли файл выгружаться через диалог сохранения или открытия файлов браузера (Content-Disposition = attachment), или браузер попытается отобразить содержимое прямо в своем окне (Content-Disposition = inline). Список расширений файлов, отображаемых в окне браузера, задается свойством приложения cuba.web.viewFileExtensions.
ExportDisplay можно также использовать для выгрузки произвольных данных, если в качестве параметра метода show() передать экземпляр класса ByteArrayDataProvider. Например:
public class SampleScreen extends AbstractWindow {
@Inject
private ExportDisplay exportDisplay;
public void onDownloadBtnClick(Component source) {
String html = "<html><head title='Test'></head><body><h1>Test</h1></body></html>";
byte[] bytes;
try {
bytes = html.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
exportDisplay.show(new ByteArrayDataProvider(bytes), "test.html", ExportFormat.HTML);
}
}
Интерфейс FileLoader предоставляет единый набор методов для работы с файловым хранилищем как на уровне Middleware, так и на клиентском уровне. Загрузка и выгрузка данных осуществляется с помощью потоков:
|
И на клиентской и на серверной стороне FileLoader работает по общему принципу: передача файлов осуществляется путём копирования данных между потоками ввода-вывода. Файлы никогда не загружаются в память целиком ни на каком уровне приложения, что позволяет передавать файлы практически любого размера. |
В качестве примера использования FileLoader представим, что нам нужно сохранять текст, введённый пользователем, в текстовый файл и отображать содержимое этого файла в другом поле на том же экране.
Экран содержит два поля textArea. Пользователь вводит текст в первое поле textArea, затем нажимает на кнопку buttonIn, и текст сохраняется в хранилище FileStorage. Второе поле будет отображать содержимое сохраненного файла по нажатию кнопки buttonOut.
Ниже представлен фрагмент XML-дескриптора экрана:
<hbox margin="true"
spacing="true">
<vbox spacing="true">
<textArea id="textAreaIn"/>
<button id="buttonIn"
caption="Save text in file"
invoke="onButtonInClick"/>
</vbox>
<vbox spacing="true">
<textArea id="textAreaOut"
editable="false"/>
<button id="buttonOut"
caption="Show the saved text"
invoke="onButtonOutClick"/>
</vbox>
</hbox>
Контроллер экрана содержит два метода, вызываемых при нажатии кнопок под текстовыми полями:
После этого мы сохраняем новый файл методом saveStream() интерфейса FileLoader, передав в него объект FileDescriptor и содержимое файла с поставщиком InputStream. Также делаем коммит объекта FileDescriptor в data store с помощью интерфейса DataManager.
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.core.global.DataManager;
import com.haulmont.cuba.core.global.FileLoader;
import com.haulmont.cuba.core.global.FileStorageException;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.ResizableTextArea;
import com.haulmont.cuba.gui.upload.FileUploadingAPI;
import org.apache.commons.io.IOUtils;
import javax.inject.Inject;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
public class FileLoaderScreen extends AbstractWindow {
@Inject
private Metadata metadata;
@Inject
private FileLoader fileLoader;
@Inject
private DataManager dataManager;
@Inject
private ResizableTextArea textAreaIn;
@Inject
private ResizableTextArea textAreaOut;
private FileDescriptor fileDescriptor;
public void onButtonInClick() {
byte[] bytes = textAreaIn.getRawValue().getBytes();
fileDescriptor = metadata.create(FileDescriptor.class);
fileDescriptor.setName("Input.txt");
fileDescriptor.setExtension("txt");
fileDescriptor.setSize((long) bytes.length);
fileDescriptor.setCreateDate(new Date());
try {
fileLoader.saveStream(fileDescriptor, () -> new ByteArrayInputStream(bytes));
} catch (FileStorageException e) {
throw new RuntimeException(e);
}
dataManager.commit(fileDescriptor);
}
public void onButtonOutClick() {
try {
InputStream inputStream = fileLoader.openStream(fileDescriptor);
textAreaOut.setValue(IOUtils.toString(inputStream));
} catch (FileStorageException | IOException e) {
throw new RuntimeException(e);
}
}
}
5.9.8.4. Стандартная реализация хранилища
Стандартная реализация хранит файлы в специальной структуре каталогов на одном или нескольких файловых ресурсах.
Корни структуры можно задать в свойстве приложения cuba.fileStorageDir. Формат - список путей через запятую. Например:
cuba.fileStorageDir=/work/sales/filestorage,/mnt/backup/filestorage
Если данное свойство не задано, хранилище будет создано в подкаталоге filestorage рабочего каталога Middleware. В стандартном варианте развертывания в Tomcat это каталог tomcat/work/app-core/filestorage.
В случае указания нескольких ресурсов хранилище ведет себя следующим образом:
Перед записью проверяется доступность каждого каталога. Если недоступен основной каталог, выбрасывается исключение и запись не производится. Если недоступен какой-то из резервных каталогов, запись все равно производится, в лог выводится сообщение об ошибке.
При недоступности основного каталога чтение производится из первого резервного каталога, в котором имеется данный файл. В лог выводится сообщение об ошибке.
Файловая структура хранилища организована следующим образом:
JMX-бин app-core.cuba:type=FileStorage отображает текущий список корней хранилища, а также предоставляет следующие методы для поиска проблем:
5.9.8.5. Реализация хранилища Amazon S3 File Storage
Вместо стандартной реализации файлового хранилища можно использовать облачные сервисы. Мы рекомендуем подключать отдельные облачные файловые хранилища при развёртывании приложений в облачных сервисах, так как последние, в большинстве своём, не гарантируют хранения внешних файлов приложения на своих жёстких дисках.
В платформе реализована поддержка файлового хранилища Amazon S3 "из коробки". Для поддержки других сервисов вам потребуется реализовать собственную логику загрузки и хранения файлов.
Для использования Amazon S3 в приложении необходимо зарегистрировать класс AmazonS3FileStorage в файле spring.xml модуля core:
<bean name="cuba_FileStorage"
class="com.haulmont.cuba.core.app.filestorage.amazon.AmazonS3FileStorage"/>
Затем нужно указать настройки хранилища Amazon в файле app.properties модуля core:
cuba.amazonS3.accessKey = <Access Key>
cuba.amazonS3.secretAccessKey = <Secret Access Key>
cuba.amazonS3.region = <Region>
cuba.amazonS3.bucket = <Bucket Name>
|
Значения accessKey и secretAccessKey необходимо взять из реквизитов пользователя, созданного в учётной записи AWS IAM, а не самой учётной записи AWS. Нужные значения ключей вы можете найти на вкладке Users консоли администратора AWS. |
Файловая структура хранилища организована таким же образом, как и в стандартной реализации хранилища.
Панель папок предназначена для быстрого доступа пользователя к часто используемой информации. Она представляет собой скрываемую панель в левой части главного окна приложения, в которой располагается иерархическая структура, нажатие на элементы которой (папки) приводит к отображению соответствующих экранов системы с определенными параметрами.
На момент написания данного руководства панель папок реализована только для Web Client.
Платформа поддерживает три вида папок: папки приложения, папки поиска и наборы записей. Папки приложения отображаются в верхней части панели в отдельной иерархии, папки поиска и наборы - в нижней части панели в совместной иерархии. Чтобы использовать в папках горячие клавиши, свойство cuba.web.foldersPaneEnabled должно иметь значение true.
На функционирование панели папок влияют следующие свойства приложения:
Для создания папок приложения пользователь должен иметь специфическое право Создание/изменение папок приложения (код cuba.gui.appFolder.global).
Простейшая папка приложения может быть создана из контекстного меню панели папок. Такая папка не связана с экранами системы и предназначена только для группировки других папок в иерархии.
Для создания папки, открывающей некоторый экран с фильтром, необходимо выполнить следующее:
Скрипт должен вернуть булевское значение. Если скрипт не задан, либо возвращает null, папка доступна. Пример:
userSession.currentOrSubstitutedUser.login == 'admin'
Скрипт должен вернуть числовое значение, целая часть которого будет использована в качестве счетчика. Если скрипт не задан, либо возвращает null, счетчик не будет отображаться. Кроме возвращаемого значения скрипт может установить переменную style, которая будет использована как имя стиля отображения папки. Пример:
def em = persistence.getEntityManager()
def q = em.createQuery('select count(o) from sales$Order o')
def count = q.getSingleResult()
style = count > 0 ? 'emphasized' : null
return count
Для отображения указанного скриптом стиля тема приложения должна содержать описание этого стиля для элемента v-tree-node внутри cuba-folders-pane, например:
.c-folders-pane .v-tree-node.emphasized {
font-weight: bold;
}
В скриптах доступны следующие переменные, установленные в контексте groovy.lang.Binding:
При обновлении папок для всех скриптов используется один экземпляр groovy.lang.Binding, поэтому между ними можно передавать переменные для исключения дублирующихся запросов и повышения производительности.
Тексты скриптов могут содержаться либо непосредственно в атрибутах сущности AppFolder, либо в отдельных файлах. В последнем случае атрибут должен содержать путь к файлу скрипта (обязательно с расширением ".groovy") по правилам интерфейса Resources. Таким образом, если содержимое атрибута представляет собой строку, заканчивающуюся на ".groovy", текст скрипта загружается из указанного файла, в противном случае в качестве скрипта используется само содержимое атрибута.
Папки приложения представляют собой экземпляры сущности AppFolder и хранятся в связанных таблицах SYS_FOLDER и SYS_APP_FOLDER.
Папки поиска создаются пользователями аналогично папкам приложения - группирующие папки непосредственно из контекстного меню панели папок, связанные с экранами - из меню кнопки Фильтр… экрана командой Сохранить как папку поиска.
Для создания глобальной папки пользователь должен иметь специфическое право Создание/изменение глобальных папок поиска (код cuba.gui.searchFolder.global).
Фильтр папки поиска можно изменить после ее создания - для этого достаточно открыть папку и в экране изменить фильтр Папка: {имя папки}. После сохранения фильтра он будет изменен и в папке тоже.
Папки поиска представляют собой экземпляры сущности SearchFolder и хранятся в связанных таблицах SYS_FOLDER и SEC_SEARCH_FOLDER.
Использование наборов в экране возможно, если для компонента Filter в атрибуте applyTo указан соответствующий компонент Table. Например:
<layout>
<filter id="customerFilter"
datasource="customersDs"
applyTo="customersTable"/>
<groupTable id="customersTable"
width="100%">
<buttonsPanel>
<button action="customersTable.create"/>
...
</buttonsPanel>
...
При этом в контекстном меню таблицы появятся команды Добавить в набор или Добавить в тек. набор / Удалить из набора. Если таблица содержит внутри себя компонент buttonsPanel (как в приведенном выше примере), команды контекстного меню будут продублированы соответствующими кнопками.
Наборы представляют собой экземпляры сущности SearchFolder и хранятся в связанных таблицах SYS_FOLDER и SEC_SEARCH_FOLDER.
5.9.10. Информация об используемом ПО
Платформа предоставляет средства для регистрации и отображения в пользовательском интерфейсе информации об используемом в приложении стороннем программном обеспечении (credits). Информация включает в себя название, ссылку на веб-сайт и текст лицензии.
Компоненты платформы содержат собственные файлы описаний cuba-credits.xml, reports-credits.xml и т.д. В проекте приложения можно создать аналогичный файл и в свойстве приложения cuba.creditsConfig указать этот файл.
Структура файла credits.xml:
Cсылаться можно на лицензии, объявленные не только в этом же файле, но и в любом другом файле, объявленном в переменной cuba.creditsConfig раньше, чем текущий.
Для отображения общего списка используемого ПО предназначен фрейм com/haulmont/cuba/gui/app/core/credits/credits-frame.xml, загружающий информацию из файлов, заданных в свойстве cuba.creditsConfig. Пример использования фрейма в экране:
<dialogMode width="500" height="400"/>
<layout expand="creditsBox">
<groupBox id="creditsBox"
caption="msg://credits"
width="100%">
<frame id="credits"
src="/com/haulmont/cuba/gui/app/core/credits/credits-frame.xml"
width="100%"
height="100%"/>
</groupBox>
</layout>
Если экран с фреймом открывается в диалоговом режиме (WindowManager.OpenType.DIALOG), ему необходимо задать высоту, иначе возможна неправильная работа скроллинга. См. элемент dialogMode в примере выше.
В состав платформы включен фреймворк MyBatis, обладающий, по сравнению с ORM и QueryRunner, более широкими возможностями по выполнению SQL и отображению результатов на объекты предметной области.
Для использования MyBatis в проекте необходимо добавить следующие бины в файл spring.xml модуля core:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="cubaDataSource"/>
<property name="configLocation" value="cuba-mybatis.xml"/>
<property name="mapperLocations" value="classpath*:com/sample/sales/core/sqlmap/*.xml"/>
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
В параметре mapperLocations задается путь (по правилам интерфейса ResourceLoader Spring) к файлам отображений MyBatis.
Пример файла отображения для загрузки экземпляра сущности Заказ вместе со связанным Покупателем и коллекцией Пунктов заказа:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sample.sales">
<select id="selectOrder" resultMap="orderResultMap">
select
o.ID as order_id,
o.DATE as order_date,
o.AMOUNT as order_amount,
c.ID as customer_id,
c.NAME as customer_name,
c.EMAIL as customer_email,
i.ID as item_id,
i.QUANTITY as item_quantity,
p.ID as product_id,
p.NAME as product_name
from
SALES_ORDER o
left join SALES_CUSTOMER c on c.ID = o.CUSTOMER_ID
left join SALES_ITEM i on i.ORDER_ID = o.id and i.DELETE_TS is null
left join SALES_PRODUCT p on p.ID = i.PRODUCT_ID
where
c.id = #{id}
</select>
<resultMap id="orderResultMap" type="com.sample.sales.entity.Order">
<id property="id" column="order_id"/>
<result property="date" column="order_date"/>
<result property="amount" column="order_amount"/>
<association property="customer" column="customer_id" javaType="com.sample.sales.entity.Customer">
<id property="id" column="customer_id"/>
<result property="name" column="customer_name"/>
<result property="email" column="customer_email"/>
</association>
<collection property="items" ofType="com.sample.sales.entity.Item">
<id property="id" column="item_id"/>
<result property="quantity" column="item_quantity"/>
<association property="product" column="product_id" javaType="com.sample.sales.entity.Product">
<id property="id" column="product_id"/>
<result property="name" column="product_name"/>
</association>
</collection>
</resultMap>
</mapper>
Для получения результатов запроса в приведенном выше примере можно использовать следующий код:
Transaction tx = persistence.createTransaction();
try {
SqlSession sqlSession = AppBeans.get("sqlSession");
Order order = (Order) sqlSession.selectOne("com.sample.sales.selectOrder", orderId);
tx.commit();
} finally {
tx.end();
}
5.9.12. Пессимистичная блокировка
Пессимистичная блокировка сущностей применяется, если велика вероятность одновременного редактирования одного и того же экземпляра, и стандартная оптимистичная блокировка, основанная на версионности сущностей, порождает слишком много коллизий.
Пессимистичная блокировка использует явное блокирование экземпляра сущности при открытии его в экране редактирования. В результате только один пользователь в некоторый момент времени может редактировать данный экземпляр сущности.
Механизм пессимистичной блокировки можно использовать также для управления совместным выполнением произвольных процессов. Причем блокировки являются распределенными, т.к. информация о них реплицируется в кластере Middleware. Подробнее см. JavaDoc интерфейсов LockManagerAPI и LockService.
Режим пессимистичной блокировки может быть задан для любого класса сущности в процессе настройки или эксплуатации системы с помощью экрана Administration > Locks > Setup, либо следующим образом:
Например:
insert into sys_lock_config (id, create_ts, name, timeout_sec) values (newid(), current_timestamp, 'sales$Order', 300)
Текущее состояние блокировок можно отслеживать через JMX-бин app-core.cuba:type=LockManager, или через специальный экран, доступный в меню Administration > Locks. Экран также позволяет разблокировать любой объект принудительно.
5.9.13. Выполнение SQL с помощью QueryRunner
QueryRunner - класс, предназначенный для выполнения SQL. Его следует использовать вместо JDBC везде, где есть необходимость работы с SQL и нежелательно применение аналогичных средств ORM.
QueryRunner платформы является вариантом Apache DbUtils QueryRunner, усовершенствованным для использования Java Generics.
Пример использования:
QueryRunner runner = new QueryRunner(persistence.getDataSource());
try {
Set<String> scripts = runner.query("select SCRIPT_NAME from SYS_DB_CHANGELOG",
new ResultSetHandler<Set<String>>() {
public Set<String> handle(ResultSet rs) throws SQLException {
Set<String> rows = new HashSet<String>();
while (rs.next()) {
rows.add(rs.getString(1));
}
return rows;
}
});
return scripts;
} catch (SQLException e) {
throw new RuntimeException(e);
}
Есть два варианта использования QueryRunner - либо в текущей транзакции, либо в отдельной в режиме autocommit.
5.9.14. Выполнение задач по расписанию
Платформа предлагает два способа запуска задач по расписанию:
5.9.14.1. Spring TaskScheduler
Данный механизм подробно описан в разделе Task Execution and Scheduling руководства Spring Framework.
TaskScheduler можно использовать для запуска методов произвольных бинов Spring в любом блоке приложения - как на Middleware, так и на клиентском уровне.
Пример конфигурации в файле spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd">
<!--...-->
<task:scheduled-tasks scheduler="scheduler">
<task:scheduled ref="sales_Processor" method="someMethod" fixed-rate="60000"/>
<task:scheduled ref="sales_Processor" method="someOtherMethod" cron="0 0 1 * * MON-FRI"/>
</task:scheduled-tasks>
</beans>
Здесь объявлены две задачи, запускающие на выполнение методы someMethod() и someOtherMethod() бина sales_Processor. При этом someMethod() запускается с момента старта приложения через фиксированные промежутки времени - 60 сек. Метод someOtherMethod() запускается в соответствии с расписанием, заданным выражением Cron (описание формата таких выражений см. http://quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger).
Собственно запуск задач выполняет бин, заданный в атрибуте scheduler элемента scheduled-tasks. Это бин класса CubaThreadPoolTaskScheduler, который сконфигурирован в модулях core и web компонента cuba (см. cuba-spring.xml, cuba-web-spring.xml). Данный класс содержит специфическую для CUBA функциональность.
Для того чтобы предоставить SecurityContext коду, выполняемому задачами Spring на среднем слое, используйте системную аутентификацию.
5.9.14.2. Назначенные задания CUBA
Механизм назначенных заданий CUBA предназначен для запуска по расписанию методов произвольных бинов Spring в блоке Middleware. Целью данного механизма и отличием его от вышеупомянутого стандартного механизма Spring Framework являются:
Под синглтон-заданием понимается задача, которая должна выполняться в некоторый момент времени только на одном сервере. Пример - чтение из очереди и отсылка email.
5.9.14.2.1. Регистрация задания
Задания регистрируются в таблице SYS_SCHEDULED_TASK базы данных, соответствующей сущности ScheduledTask. Для работы с заданиями существуют экраны просмотра и редактирования, доступные через меню Администрирование → Назначенные задания.
Рассмотрим атрибуты задания:
|
Бин отображается в списке и доступен для выбора, только если он объявлен в модуле core и у него есть интерфейс, содержащий подходящие для вызова из задания методы. Бины без интерфейса не поддерживаются. |
Start date имеет смысл указывать только для "нечастых" заданий - раз в 1 час, 1 сутки и т.п.
Если Start date не указано, то Time frame не принимается во внимание, т.е. задание будет запущено в любое время после прохождения промежутка времени Period после предыдущего выполнения задания.
Для синглтон-заданий порядок перечисления серверов указывает их приоритет - первый имеет больший приоритет чем последний. Сервер с большим приоритетом перехватит выполнение синглтона следующим образом: если сервер с большим приоритетом обнаруживает, что предыдущий раз задание было выполнено сервером с меньшим приоритетом, то он запускает задание независимо от того, пройден ли Period или нет.
|
Приоритет серверов работает только в случае Scheduling type равного Period и не указанного атрибута Start date. В противном случае, старт происходит в одно и то же время, и перехват невозможен. |
Если задание является синглтоном, то в текущей реализации регистрация факта запуска производится в любом случае, независимо от данного признака.
Если задание является синглтоном, то в текущей реализации регистрация факта завершения производится в любом случае, независимо от данного признака.
Задание также имеет признак активности, который устанавливается в экране списка заданий. Неактивные задания не запускаются.
5.9.14.2.2. Управление обработкой заданий
Данный метод можно вызывать автоматически, для этого достаточно создать новое задание и установить для него следующие параметры:
5.9.14.2.3. Особенности реализации
|
Для нормальной работы синглтон-заданий необходима точная синхронизация серверов Middleware по времени! |
Блок Web Client позволяет открывать экраны приложения по команде, переданной в URL. Причем если в данный момент в браузере нет сессии приложения с зарегистрированным пользователем, то сначала будет отображено окно логина, и сразу после успешной регистрации - главное окно приложения с требуемым экраном.
Набор возможных команд указывается в свойстве приложения cuba.web.linkHandlerActions, по умолчанию это команды open и o. При обработке HTTP запроса анализируется последняя часть URL, и если она совпадает с одной из команд, управление передается в соответствующий процессор, который является бином, реализующим интерфейс LinkHandlerProcessor.
Платформа предоставляет процессор, принимающий следующие параметры запроса:
http://localhost:8080/app/open?screen=sec$User.browse
http://localhost:8080/app/open?screen=sec$User.edit&item=sec$User-60885987-1b61-4247-94c7-dff348347f93
http://localhost:8080/app/open?screen=sec$User.edit&item=sec$User-60885987-1b61-4247-94c7-dff348347f93-user.edit
Для открытия экрана создания нового экземпляра сущности в данном параметре нужно передать строку вида NEW-entityName:
http://localhost:8080/app/open?screen=sec$User.edit&item=NEW-sec$User
http://localhost:8080/app/open?screen=sales$Customer.lookup¶ms=p1:v1,p2:v2
http://localhost:8080/app/open?screen=sales$Customer.lookup¶ms=p1:sales$Customer-01e37691-1a9b-11de-b900-da881aea47a6
Если вам необходимо добавить обработку специфических команд, выполните следующие шаги:
Ваш бин может опционально реализовывать интерфейс Ordered или содержать аннотацию Order фреймворка Spring. Тогда вы сможете указать позицию вашего бина в цепочке процессоров. Используйте константы HIGHEST_PLATFORM_PRECEDENCE и LOWEST_PLATFORM_PRECEDENCE интерфейса LinkHandlerProcessor чтобы поместить ваш бин до или после процессоров, определенных в платформе. Так, если вы укажете порядок числом, меньшим HIGHEST_PLATFORM_PRECEDENCE, ваш бин будет запрошен раньше, и вы при необходимости сможете переопределить поведение, заданное процессором платформы.
5.9.16. Генерация последовательностей
Данный механизм позволяет генерировать уникальные последовательности чисел через единый API, независимо от используемой СУБД.
Основной частью данного механизма является бин UniqueNumbers с интерфейсом UniqueNumbersAPI, доступный в блоке Middleware. Методы интерфейса:
Последовательности не требуют предварительной инициализации - при первом вызове getNextNumber() соответствующая последовательность будет создана и вернет значение 1.
Пример получения следующего значения последовательности в бине блока Middleware:
@Inject
private UniqueNumbersAPI uniqueNumbers;
private long getNextValue() {
return uniqueNumbers.getNextNumber("mySequence");
}
Для получения значений последовательностей в клиентских блоках используется метод getNextNumber() сервиса UniqueNumbersService.
Для управления последовательностями можно использовать JMX-бин app-core.cuba:type=UniqueNumbers с методами, дублирующими методы UniqueNumbersAPI.
Реализация механизма генерации последовательностей зависит от типа используемой СУБД. Поэтому параметрами последовательностей можно также управлять непосредственно в БД, но разными способами.
5.9.17. Журналирование пользовательских сессий
Механизм журналирования предназначен для отслеживания факта входа пользователей в систему. В журнале администратор системы может найти информацию, кто и когда вошёл в систему и вышел из неё. Механизм основан на отслеживании пользовательских сессий. При каждом создании объекта UserSession в базу данных сохраняется следующая информация:
По умолчанию записи о пользовательских сессиях не сохраняются. Простейший способ активировать журналирование - воспользоваться кнопкой Enable Logging в экране приложения Administration > User Session Log. В качестве альтернативы можно установить в true значение свойства приложения cuba.UserSessionLogEnabled и перезапустить сервер.
При необходимости можно создать отчёт для сущности sec$SessionLogEntry.
5.10. Расширение функциональности
Платформа позволяет расширять и переопределять свою функциональность в приложениях в следующих аспектах:
Рассмотрим две первые задачи на примере добавления поля Адрес в сущность User подсистемы безопасности платформы.
Создадим в проекте приложения класс сущности, унаследованный от com.haulmont.cuba.security.entity.User и добавим в него требуемый атрибут с соответствующими методами доступа:
@Entity(name = "sales$ExtUser")
@Extends(User.class)
public class ExtUser extends User {
@Column(name = "ADDRESS", length = 100)
private String address;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
В аннотации @Entity должно быть указано новое имя сущности. Так как базовая сущность не объявляет стратегию наследования, то по умолчанию это SINGLE_TABLE. Это означает, что унаследованная сущность будет храниться в той же таблице, что и базовая, и аннотация @Table не требуется. Другие аннотации базовой сущности - @NamePattern, @Listeners и прочие - автоматически применяются к расширяющей сущности, но могут быть переопределены в ее классе.
Важным элементом класса новой сущности является аннотация @Extends с базовым классом в качестве параметра. Она позволяет сформировать реестр расширяющих сущностей, и заставить механизмы платформы использовать их повсеместно вместо базовых. Реестр реализуется классом ExtendedEntities, который является бином Spring с именем cuba_ExtendedEntities, и доступен также через интерфейс Metadata.
Добавим локализованное название нового атрибута в пакет com.sample.sales.entity:
messages.properties
ExtUser.address=Address
messages_ru.properties
ExtUser.address=Адрес
Зарегистрируем новую сущность в файле persistence.xml проекта:
<class>com.sample.sales.entity.ExtUser</class>
Добавим в скрипты создания и обновления базы данных команду модификации соответствующей таблицы:
-- add column for "address" attribute
alter table SEC_USER add column ADDRESS varchar(100)
^
-- add discriminator column required for entity inheritance
alter table SEC_USER add column DTYPE varchar(100)
^
-- set discriminator value for existing records
update SEC_USER set DTYPE = 'sales$ExtUser' where DTYPE is null
^
Чтобы использовать новые атрибуты сущности в экранах, создадим представления для новой сущности с теми же именами, что у представлений базовой сущности. Новые представления должны расширять базовые и определять новые атрибуты. Например:
<view class="com.sample.sales.entity.ExtUser"
name="user.browse"
extends="user.browse">
<property name="address"/>
</view>
Расширяющее представление включает в себя все атрибуты базового представления. Расширяющее представление не требуется, если базовое расширяет _local и вы добавляете только локальные атрибуты. Поэтому в описываемом случае данный шаг может быть пропущен.
Платформа позволяет создавать новые XML-дескрипторы экранов путем наследования от существующих.
Наследование XML выполняется путем указания в корневом элементе window атрибута extends, содержащего путь к базовому дескриптору.
Правила переопределения элементов XML экрана:
Для отладки преобразования дескрипторов можно включить вывод в журнал сервера результирующего XML. Делается это путем указания уровня TRACE для логгера com.haulmont.cuba.gui.xml.XmlInheritanceProcessor в файле конфигурации Logback.
Пример XML-дескриптора экрана браузера сущностей ExtUser:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
extends="/com/haulmont/cuba/gui/app/security/user/browse/user-browse.xml">
<layout>
<groupTable id="usersTable">
<columns>
<column id="address" ext:index="2"/>
</columns>
</groupTable>
</layout>
</window>
В данном примере дескриптор унаследован от стандартного браузера сущностей User платформы, и в таблицу добавлена колонка address с индексом 2, т.е. отображающаяся после login и name.
Зарегистрируем новый экран в screens.xml с теми же идентификаторами, которые использовались для базового экрана. После этого новый экран будет повсеместно вызываться взамен старого.
<screen id="sec$User.browse"
template="com/sample/sales/gui/extuser/extuser-browse.xml"/>
<screen id="sec$User.lookup"
template="com/sample/sales/gui/extuser/extuser-browse.xml"/>
Аналогично создаем экран редактирования:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
extends="/com/haulmont/cuba/gui/app/security/user/edit/user-edit.xml">
<layout>
<fieldGroup id="fieldGroup">
<column id="fieldGroupColumn2">
<field property="address" ext:index="4"/>
</column>
</fieldGroup>
</layout>
</window>
Регистрируем его в screens.xml с идентификатором базового экрана:
<screen id="sec$User.edit"
template="com/sample/sales/gui/extuser/extuser-edit.xml"/>
После выполнения описанных выше действий в приложении вместо платформенной сущности User будет использоваться ExtUser с соответствующими экранами.
Контроллер экрана может быть расширен путем создания нового класса, унаследованного от контроллера базового экрана. Имя класса указывается в атрибуте class корневого элемента расширяющего XML дескриптора, при этом выполняются обычные правила наследования XML, описанные выше.
5.10.3. Расширение бизнес-логики
Основная часть бизнес-логики платформы сосредоточена в бинах Spring, что позволяет легко расширить или переопределить ее в приложении.
Для подмены реализации бина достаточно создать свой класс, реализующий интерфейс или расширяющий базовый класс платформы, и зарегистрировать его в spring.xml приложения. Аннотацию @Component в расширяющем классе применять нельзя, переопределение бинов возможно только с помощью конфигурации в XML.
Рассмотрим пример добавления метода в бин PersistenceTools.
Создаем класс с нужным методом:
public class ExtPersistenceTools extends PersistenceTools {
public Entity reloadInSeparateTransaction(final Entity entity, final String... viewNames) {
Entity result = persistence.createTransaction().execute(new Transaction.Callable<Entity>() {
@Override
public Entity call(EntityManager em) {
return em.reload(entity, viewNames);
}
});
return result;
}
}
Регистрируем класс в spring.xml модуля core проекта с тем же идентификатором, что и бин платформы:
<bean id="cuba_PersistenceTools" class="com.sample.sales.core.ExtPersistenceTools"/>
После этого контекст Spring вместо экземпляра базового класса PersistenceTools будет всегда возвращать ExtPersistenceTools, например:
Persistence persistence;
PersistenceTools tools;
persistence = AppBeans.get(Persistence.class);
tools = persistence.getTools();
assertTrue(tools instanceof ExtPersistenceTools);
tools = AppBeans.get(PersistenceTools.class);
assertTrue(tools instanceof ExtPersistenceTools);
tools = AppBeans.get(PersistenceTools.NAME);
assertTrue(tools instanceof ExtPersistenceTools);
Таким же образом можно переопределять логику сервисов, например, из компонентов приложения: для подмены реализации бина нужно создать класс, расширяющий функциональность исходного сервиса. В следующем примере мы создали новый класс NewOrderServiceBean, чтобы переопределить в нём метод из исходного сервиса OrderServiceBean:
public class NewOrderServiceBean extends OrderServiceBean {
@Override
public BigDecimal calculateOrderAmount(Order order) {
BigDecimal total = super.calculateOrderAmount(order);
BigDecimal vatPercent = new BigDecimal(0.18);
return total.multiply(BigDecimal.ONE.add(vatPercent));
}
}
Теперь, после регистрации нового класса в spring.xml, новая реализация будет использоваться в приложении вместо исходной, заданной в OrderServiceBean. Обратите внимание, что в определении бина используется идентификатор из компонента приложения и полное имя нового класса:
<bean id="workshop_OrderService" class="com.company.retail.service.NewOrderServiceBean"/>
5.10.4. Регистрация сервлетов и фильтров
Чтобы использовать сервлеты и фильтры Spring Security, настроенные в компоненте приложения, их нужно зарегистрировать из компонента так, чтобы регистрация динамически распространялась и на родительское приложение. По умолчанию, в рамках одного приложения сервлеты регистрируются в файле конфигурации web.xml, но в случае с подключением компонентов такой подход не работает.
Для динамической регистрации сервлетов и фильтров используется бин ServletRegistrationManager: он гарантирует, что при загрузке каждого сервлета будет использован корректный ClassLoader, и позволяет обращаться к статическим классам, таким как AppContext. Этот бин необходимо использовать для корректной работы компонентов независимо от варианта развёртывания приложения.
Бин ServletRegistrationManager имеет два метода:
Для использования этого бина мы рекомендуем создать в компоненте приложения отдельный бин-инициализатор. Этот бин должен представлять собой обычный класс с аннотацией @Component и содержать слушатели событий создания и уничтожения контекста приложения: ServletContextInitializedEvent и ServletContextDestroyedEvent.
Пример бина-инициализатора:
@Component
public class WebInitializer {
@Inject
private ServletRegistrationManager servletRegistrationManager;
@EventListener
public void initializeHttpServlet(ServletContextInitializedEvent e) {
Servlet myServlet = servletRegistrationManager.createServlet(e.getApplicationContext(), "com.demo.comp.MyHttpServlet");
e.getSource().addServlet("my_servlet", myServlet)
.addMapping("/myservlet/");
}
}
Здесь класс WebInitializer содержит только один слушатель, который используется для регистрации HTTP-сервлета из компонента приложения в родительском приложении.
Метод createServlet() принимает контекст приложения, полученный из события ServletContextInitializedEvent, и полное имя класса HTTP-сервлета. Далее мы регистрируем сервлет по его имени (my_servlet) и указываем URL, по которому будет доступен сервлет (/myservlet/). Теперь при подключении этого компонента к другому приложению сервлет MyHttpServlet будет зарегистрирован сразу после инициализации AppContext и ServletContext.
Более сложный пример использования бина ServletRegistrationManager приведён в разделе Регистрация DispatcherServlet из компонента приложения.
Регистрация сервлетов для развертывания в единый WAR-файл
Для корректной загрузки сервлетов и фильтров при развертывании в единый WAR-файл следуйте инструкции ниже:
public class CustomWebListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
ServletContext servletContext = servletContextEvent.getServletContext();
registerServlet(servletContext);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
protected void registerServlet(ServletContext servletContext) {
Servlet testServlet = new TestServlet();
ServletRegistration.Dynamic servletReg = servletContext.addServlet("test_servlet", cubaServlet);
servletReg.setLoadOnStartup(0);
servletReg.setAsyncSupported(true);
servletReg.addMapping("/testServlet");
}
}
<context-param>
<param-name>webServletContextListener</param-name>
<param-value>com.company.CustomWebListener</param-value>
</context-param>
Данная глава содержит практическую информацию по созданию приложений на основе платформы.
Форматирование кода
Максимальная длина строки − 120 символов. Длина отступа - 4 символа, использование пробелов вместо символов табуляции включено.
Соглашения по именованию
|
Идентификатор |
Правило именования |
Пример |
|
Java и Groovy классы | ||
|
Класс контроллера экрана |
UpperCamelCase Контроллер экрана списка сущностей − {КлассСущности}Browse Контроллер экрана редактирования − {КлассСущности}Edit |
CustomerBrowse OrderEdit |
|
XML дескрипторы экранов | ||
|
Идентификатор компонента, имена параметров в запросах |
lowerCamelCase, только буквы и цифры |
attributesTable :component$relevantTo :ds$attributesDs |
|
Идентификатор источника данных |
lowerCamelCase, только буквы и цифры, оканчивается на Ds |
attributesDs |
|
SQL скрипты | ||
|
Зарезервированные слова |
lowercase |
create table |
|
Таблицы |
UPPER_CASE. Название предваряется именем проекта для формирования пространства имен. В именах таблиц рекомендуется использовать единственное число. |
SALES_CUSTOMER |
|
Колонки |
UPPER_CASE |
CUSTOMER TOTAL_AMOUNT |
|
Колонки внешних ключей |
UPPER_CASE. Состоит из имени таблицы, на которую ссылается колонка (без префикса проекта), и суффикса _ID. |
CUSTOMER_ID |
|
Индексы |
UPPER_CASE. Состоит из префикса IDX_, имени таблицы, для которой создается индекс (с префиксом проекта), и имен полей, включенных в индекс. |
IDX_SALES_CUSTOMER_NAME |
6.2. Файловая структура проекта
Рассмотрим файловую структуру проекта на примере простого приложения Sales, состоящего из блоков Middleware, Web Client и Web Portal.
Рисунок 36. Файловая структура проекта
В корне проекта расположены скрипты сборки build.gradle, settings.gradle и проектные файлы IntelliJ IDEA.
В каталоге modules расположены подкаталоги модулей проекта − global, core, gui, portal, web.
Рисунок 37. Структура модуля global
Модуль global содержит каталог исходных текстов src, в корне которого располагаются конфигурационные файлы metadata.xml, persistence.xml и views.xml. Пакет com.sample.sales.service содержит интерфейсы сервисов Middleware, пакет com.sample.sales.entity - классы сущностей и файлы локализации для них.
Рисунок 38. Структура модуля core
Модуль core содержит следующие каталоги:
Рисунок 39. Структура модуля gui
Модуль gui содержит каталог исходных текстов src, в корне которого располагается конфигурационный файл screens.xml. Пакет com.sample.sales.gui содержит XML-дескрипторы и контроллеры экранов и файлы локализации для них.
Рисунок 40. Структура модуля web
Модуль web содержит следующие каталоги:
Для сборки проектов на основе платформы используется система сборки Gradle. Скрипты сборки представляют собой два файла в корневом каталоге проекта:
В данном разделе описывается структура скриптов, а также предназначение и параметры задач (tasks) Gradle.
В данном разделе описывается структура и основные элементы скрипта build.gradle.
buildscript
В секции buildscript задается следующее:
После секции buildscript объявляются несколько переменных, используемых далее в скрипте.
cuba
Логика сборки, специфичная для CUBA, сосредоточена в Gradle плагине cuba. Он подключается в корне скрипта и в секциях configure каждого модуля:
apply(plugin: 'cuba')
Параметры плагина cuba задаются в секции cuba:
cuba {
artifact {
group = 'com.company.sales'
version = '0.1'
isSnapshot = true
}
tomcat {
dir = "$project.rootDir/build/tomcat"
}
ide {
copyright = '...'
classComment = '...'
vcs = 'Git'
}
}
Рассмотрим доступные параметры:
Версию артефактов можно переопределить в командной строке, например:
gradle assemble -Pcuba.artifact.version=1.1.1
Параметры репозитория выгрузки артефактов можно передать в командной строке с помощью следующих аргументов:
gradlew uploadArchives -PuploadUrl=http://myrepo.com/content/repositories/snapshots -PuploadUser=me -PuploadPassword=mypassword
dependencies
Данная секция описывает набор компонентов приложения, используемых в проекте. Компоненты указываются координатами артефакта их модуля global. В примере ниже используются три компонента: com.haulmont.cuba (компонент cuba платформы), com.haulmont.reports (премиум-дополнение reports) и com.company.base (кастомный компонент):
dependencies {
appComponent("com.haulmont.cuba:cuba-global:$cubaVersion")
appComponent("com.haulmont.reports:reports-global:$cubaVersion")
appComponent("com.company.base:base-global:0.1-SNAPSHOT")
}
configure
Секции configure описывают конфигурацию модулей. Наиболее важная часть конфигурации - описание зависимостей, например:
configure(coreModule) {
dependencies {
// standard dependencies using variables defined in the script above
compile(globalModule)
provided(servletApi)
jdbc(hsql)
testRuntime(hsql)
// add a custom repository-based dependency
compile('com.company.foo:foo:1.0.0')
// add a custom file-based dependency
compile(files("${rootProject.projectDir}/lib/my-library-0.1.jar"))
// add all JAR files in the directory to dependencies
compile(fileTree(dir: 'libs', include: ['*.jar']))
}
Блок entitiesEnhancing позволяет описать конфигурацию bytecode enhancement (weaving) классов сущностей. Его нужно объявить как минимум в модуле global, однако можно включить его в конфигурацию каждого модуля по-отдельности.
Секции main и test здесь - это наборы исходников проекта и тестов, а необязательный параметр persistenceConfig позволяет явно указать набор файлов persistence.xml. Если не установлено, задача будет обрабатывать все персистентные сущности, перечисленные в файлах *persistence.xml, найденных в CLASSPATH.
configure(coreModule) {
...
entitiesEnhancing {
main {
enabled = true
persistenceConfig = 'custom-persistence.xml'
}
test {
enabled = true
persistenceConfig = 'test-persistence.xml'
}
}
}
Нестандартные зависимости модулей можно задавать в Studio на вкладке Project properties > Advanced. См. также контекстную помощь Studio.
|
В случае транзитивных зависимостей и конфликта версий будет использована стандартная стратегия разрешения версий Maven. Согласно этой стратегии, релизные версии имеют приоритет над snapshot-версиями, а более точный числовой квалификатор имеет приоритет над более общим. При прочих равных, строковые квалификаторы приоритизируются в алфавитном порядке. Пример: 1.0-beta1-SNAPSHOT // низкий приоритет 1.0-beta1 1.0-beta2-SNAPSHOT | 1.0-rc1-SNAPSHOT | 1.0-rc1 | 1.0-SNAPSHOT | 1.0 | 1.0-sp V 1.0-whatever 1.0.1 // высокий приоритет |
6.3.2. Настройка доступа к репозиторию
Основной репозиторий
При создании нового проекта в CUBA Studio вам необходимо выбрать основной репозиторий, содержащий артефакты платформы. По умолчанию имеется два таких репозитория (может быть больше если установлен приватный репозиторий):
Оба репозитория имеют идентичное содержимое для последних версий платформы, но Bintray не содержит снэпшотов. Мы предполагаем, что Bintray является более надежным для доступа из любой точки мира.
В случае Bintray, скрипт сборки сконфигурирован также для использования репозиториев Maven Central, JCenter и Vaadin Add-ons по отдельности.
Доступ к премиум-дополнениям CUBA
Если ваш проект использует премиум-дополнения, Studio добавляет еще один репозиторий:
Если используется приватный репозиторий, автоматически ничего не добавляется, так как предполагается, что он проксирует все CUBA-репозитории, включая премиум.
Оба репозитория премиум-дополнений требуют указания имени пользователя и пароля, которые предоставляются по подписке на разработчика. Первая часть лицензионного ключа до тире представляет собой имя пользователя, вторая часть после тире - пароль. Например, если ваш ключ 111111222222-abcdefabcdef, то имя - 111111222222, пароль - abcdefabcdef
Studio передает Gradle параметры подключения когда запускает скрипт сборки. При сборке проекта вне Studio передайте premiumRepoUser и premiumRepoPass в командной строке в аргументах -P. В случае Bintray, к имени пользователя нужно в конце добавить @cuba-platform.
Пример сборки используя repo.cuba-platform.com:
gradlew assemble -PpremiumRepoUser=111111222222 -PpremiumRepoPass=abcdefabcdef
Пример сборки используя Bintray:
gradlew assemble -PpremiumRepoUser=111111222222@cuba-platform -PpremiumRepoPass=abcdefabcdef
Чтобы при сборке проекта из командной строки не передавать имя и пароль каждый раз, можно однократно задать их одним из следующих способов.
premiumRepoUser=111111222222
premiumRepoPass=abcdefabcdef
Проект может использовать любые дополнительные репозитории, содержащие компоненты приложения. Они должны быть вручную указаны в build.gradle после основного репозитория, например:
repositories {
// main repository containing CUBA artifacts
maven {
url 'https://repo.cuba-platform.com/content/groups/work'
credentials {
// ...
}
}
// custom repository
maven {
url 'http://localhost:8081/repository/maven-snapshots'
}
}
Исполняемыми единицами в Gradle являются задачи (tasks). Они задаются как внутри плагинов, так и в самом скрипте сборки. Рассмотрим специфические для CUBA задачи, параметры которых могут быть сконфигурированы в build.gradle.
Задача buildInfo автоматически добавляется в конфигурацию модуля global плагином CUBA для Gradle. Она записывает файл build-info.properties с информацией о приложении в артефакт global (например, app-global-1.0.0.jar). Во время работы приложения, эта информация читается бином BuildInfo и отображается на экране Help > About. Данный бин может также вызываться другими механизмами для получения информации о имени, версии и т.д. приложения.
В проекте можно явно указать следующие параметры задачи:
Пример указания параметров задачи:
configure(globalModule) {
buildInfo {
appName = 'MyApp'
properties = ['prop1': 'val1', 'prop2': 'val2']
}
// ...
buildUberJar – задача типа CubaUberJarBuilding, выполняющая сборку приложения и его зависимостей в JAR-файл вместе со встроенным HTTP-сервером Jetty. Можно создать либо один all-in-one JAR, либо несколько по числу блоков приложения, используемых в проекте, например, app-core.jar для Middleware и app.jar для Web Client.
Задача должна быть объявлена в корне скрипта build.gradle. Собранные JAR-файлы находятся в подкаталоге build/distributions проекта. Руководство по запуску собранных JAR-файлов смотрите в разделе Развертывание UberJAR.
|
Эту задачу можно настроить на странице Deployment settings > Uber JAR в Studio. См. контекстную помощь. |
Параметры задачи:
task buildUberJar(type: CubaUberJarBuilding) {
coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml'
// ...
}
Вы можете передавать разные настройки подключения к БД для одного UberJar во время работы приложения, используя несколько файлов jetty-env.xml и аргумент командной строки -jettyEnvPath.
task buildUberJar(type: CubaUberJarBuilding) {
appProperties = ['cuba.automaticDatabaseUpdate' : true]
// ...
}
task buildUberJar(type: CubaUberJarBuilding) {
singleJar = true
// ...
}
task buildUberJar(type: CubaUberJarBuilding) {
appName = 'sales'
// ...
}
После изменения имени приложения на sales задача создаст файлы sales-core.jar и sales.jar, и веб-клиент будет доступен по адресу http://localhost:8080/sales. Вы также можете изменить веб-контекст (последнюю часть URL после /) во время работы приложения, не изменяя заранее имени приложения, с помощью аргумента командной строки -contextName, или просто переименовав сам JAR файл.
Например:
logbackConfigurationFile = "/modules/global/src/logback.xml"
buildWar - задача типа CubaWarBuilding, выполняющая сборку приложения и его зависимостей в WAR-файл. Должна быть объявлена в корне скрипта build.gradle. Собранные WAR-файлы находятся в подкаталоге build/distributions проекта.
|
Эту задачу можно настроить на странице Deployment settings > WAR в Studio. См. контекстную помощь. |
Любое CUBA-приложение состоит как минимум из двух блоков: Middleware и Web Client. Поэтому наиболее естественный способ развертывания приложения это создание двух файлов WAR: один для Middleware, второй для Web Client. Это также позволяет масштабировать приложение при увеличении нагрузки. Однако раздельные WAR-файлы содержат дублированные зависимости, что увеличивает их общий размер. Кроме того, часто расширенные возможности развертывания не нужны и только усложняют процесс. Задача CubaWarBuilding может создавать WAR-файлы обоих типов: один файл на блок или единственный WAR, содержащий оба блока. В последнем случае блоки приложения загружаются в раздельные class loaders внутри одного веб-приложения.
Создание раздельных WAR-файлов для Middleware и Web Client
Для создания двух отдельных WAR-файлов для Middleware и Web Client используйте следующую конфигурацию:
task buildWar(type: CubaWarBuilding) {
appHome = '${app.home}'
appProperties = ['cuba.automaticDatabaseUpdate': 'true']
singleWar = false
}
Параметры задачи:
Пример использования собственных web.xml:
task buildWar(type: CubaWarBuilding) {
singleWar = false
// ...
coreWebXmlPath = 'modules/core/web/WEB-INF/production-web.xml'
webWebXmlPath = 'modules/web/web/WEB-INF/production-web.xml'
}
Например:
logbackConfigurationFile = "/modules/global/src/logback.xml"
Для создания единого файла WAR, включающего в себя блоки Middleware и Web Client, используйте следующую конфигурацию:
task buildWar(type: CubaWarBuilding) {
appHome = '${app.home}'
webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml'
}
Следующие параметры должны быть указаны в дополнение к описанным выше:
Пример файла single-war-web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!--Application components-->
<context-param>
<param-name>appComponents</param-name>
<param-value>com.haulmont.cuba</param-value>
</context-param>
<!-- Web Client parameters -->
<context-param>
<description>List of app properties files for Web Client</description>
<param-name>appPropertiesConfigWeb</param-name>
<param-value>
classpath:com/company/sample/web-app.properties
/WEB-INF/local.app.properties
</param-value>
</context-param>
<context-param>
<description>Web resources version for correct caching in browser</description>
<param-name>webResourcesTs</param-name>
<param-value>${webResourcesTs}</param-value>
</context-param>
<!-- Middleware parameters -->
<context-param>
<description>List of app properties files for Middleware</description>
<param-name>appPropertiesConfigCore</param-name>
<param-value>
classpath:com/company/sample/app.properties
/WEB-INF/local.app.properties
</param-value>
</context-param>
<!-- Servlet context listeners that load the application blocks -->
<listener>
<listener-class>
com.vaadin.server.communication.JSR356WebsocketInitializer
</listener-class>
</listener>
<listener>
<listener-class>
com.haulmont.cuba.core.sys.singleapp.SingleAppCoreServletListener
</listener-class>
</listener>
<listener>
<listener-class>
com.haulmont.cuba.web.sys.singleapp.SingleAppWebServletListener
</listener-class>
</listener>
</web-app>
Все фильтры и сервлеты при развёртывании в единый WAR-файл необходимо программно зарегистрировать, см. Регистрация сервлетов для развертывания в единый WAR-файл.
Единый WAR файл содержит только модули core и web (Middleware и Web Client). Для развертывания модуля portal используйте раздельные WAR-файлы.
В разделе Развертывание WAR в Jetty содержатся пошаговые инструкции по некоторым вариантам развертывания WAR-файлов.
buildWidgetSet - задача типа CubaWidgetSetBuilding, которая собирает кастомный GWT widgetset если в проекте есть модуль web-toolkit. Данный модуль позволяет разрабатывать собственные визуальные компоненты.
Доступные параметры:
Пример использования:
task buildWidgetSet(type: CubaWidgetSetBuilding) {
widgetSetClass = 'com.company.sample.web.toolkit.ui.AppWidgetSet'
style = 'PRETTY'
}
createDb - задача типа CubaDbCreation, создающая базу данных приложения путем выполнения соответствующих скриптов. Объявляется в модуле core. Параметры:
Пример для PostgreSQL:
task createDb(dependsOn: assemble, description: 'Creates local database', type: CubaDbCreation) {
dbms = 'postgres'
dbName = 'sales'
dbUser = 'cuba'
dbPassword = 'cuba'
}
Пример для MS SQL Server:
task createDb(dependsOn: assemble, description: 'Creates local database', type: CubaDbCreation) {
dbms = 'mssql'
dbName = 'sales'
dbUser = 'sa'
dbPassword = 'saPass1'
connectionParams = ';instance=myinstance'
}
Пример для Oracle:
task createDb(dependsOn: assemble, description: 'Creates database', type: CubaDbCreation) {
dbms = 'oracle'
host = '192.168.1.10'
dbName = 'orcl'
dbUser = 'sales'
dbPassword = 'sales'
oracleSystemPassword = 'manager'
driverClasspath = "$tomcatDir/lib/ojdbc6.jar"
}
debugWidgetSet - задача типа CubaWidgetSetDebug, которая запускает GWT Code Server для отладки виджетов в веб-браузере.
Пример использования:
task debugWidgetSet(type: CubaWidgetSetDebug) {
widgetSetClass = 'com.company.sample.web.toolkit.ui.AppWidgetSet'
}
Убедитесь, что кофигурация runtime модуля web-toolkit содержит зависимость от библиотеки Servlet API:
configure(webToolkitModule) {
dependencies {
runtime(servletApi)
}
...
См. Отладка виджетов в веб-браузере для получения информации о том как отлаживать код в веб-браузере.
deploy - задача типа CubaDeployment, выполняющая быстрое развертывание модуля в Tomcat. Объявляется в модулях core, web, portal. Параметры:
Например:
task deploy(dependsOn: assemble, type: CubaDeployment) {
appName = 'app-core'
jarNames = ['cuba-global', 'cuba-core', 'app-global', 'app-core']
}
deployThemes - задача типа CubaDeployThemeTask, выполняющая сборку и развертывание определенных в проекте тем в запущенное веб-приложение, развернутое задачей deploy. Изменения в темах применяются без рестарта сервера.
Например:
task deployThemes(type: CubaDeployThemeTask, dependsOn: buildScssThemes) {
}
deployWar - задача типа CubaJelasticDeploy, выполняющая развёртывание WAR-файла на сервер Jelastic.
|
Эту задачу можно настроить на странице Deployment settings > Cloud в Studio. См. контекстную помощь. |
Пример использования:
task deployWar(type: CubaJelasticDeploy, dependsOn: buildWar) {
email = '<your@email.address>'
password = '<your password>'
context = '<app contex>'
environment = '<environment name or ID>'
hostUrl = '<Host API url>'
}
Параметры задачи:
restart - задача, выполняющая остановку, быстрое развертывание и старт локального сервера Tomcat.
setupTomcat - задача типа CubaSetupTomcat, выполняющая установку и инициализацию локального сервера Tomcat для последующего быстрого развертывания приложения. Эта задача автоматически добавляется в проект при подключении плагина сборки cuba, поэтому объявлять ее в build.gradle не нужно. Каталог установки Tomcat задается свойством tomcat.dir секции cuba. По умолчанию это подкаталог build/tomcat проекта.
start - задача типа CubaStartTomcat, выполняющая запуск локального сервера Tomcat, установленного задачей setupTomcat. Эта задача автоматически добавляется в проект при подключении плагина cuba, поэтому объявлять ее в build.gradle не нужно.
startDb - задача типа CubaHsqlStart, выполняющая запуск локального сервера HSQLDB. Параметры:
Например:
task startDb(type: CubaHsqlStart) {
dbName = 'sales'
}
stop - задача типа CubaStopTomcat, выполняющая остановку локального сервера Tomcat, установленного задачей setupTomcat. Эта задача автоматически добавляется в проект при подключении плагина cuba, поэтому объявлять ее в build.gradle не нужно.
stopDb - задача типа CubaHsqlStop, выполняющая остановку локального сервера HSQLDB. Параметры аналогичны задаче startDb.
tomcat – задача типа Exec, выполняющая запуск локального сервера Tomcat в текущем окне терминала, которое остаётся открытым даже в случае ошибок при старте. Это упрощает диагностику ошибок запуска Tomcat, например, связанных с версией Java.
updateDb - задача типа CubaDbUpdate, обновляющая базу данных приложения путем выполнения соответствующих скриптов. Аналогична задаче createDb, за исключением отсутствия параметров dropDbSql и createDbSql.
zipProject - задача типа CubaZipProject, создающая ZIP-архив проекта. Архив не будет содержать проектные файлы IDE, результаты сборки и сервер Tomcat. Однако база данных HSQL включается в архив (если присутствует в подкаталоге build).
Эта задача автоматически добавляется в проект при подключении плагина сборки cuba, поэтому объявлять ее в build.gradle не нужно.
Задачи (tasks) Gradle, описанные в скриптах сборки, запускаются на исполнение следующими способами:
|
Рекомендуется запускать команды gradlew или gradle с ключом --daemon, в этом случае демон Gradle остается в памяти и существенно ускоряет последующее выполнение. Для удаления демона из памяти используется ключ --stop |
Например, чтобы выполнить компиляцию Java файлов и сборку JAR файлов артефактов проекта, необходимо запустить следующую команду:
gradlew --daemon assemble
|
Если ваш проект использует премиум-дополнения, и вы запускаете сборку вне Studio, необходимо передать в Gradle имя и пароль доступа к премиум-репозиторию. См. раздел выше для получения подробной информации. |
Рассмотрим типичные задачи сборки в обычном порядке их использования.
6.3.5. Установка приватного репозитория артефактов
В этом разделе рассказывается, как установить приватный Maven репозиторий, чтобы использовать его для хранения артефактов платформы и других зависимостей, вместо публичного репозитория CUBA. Это рекомендуется делать в следующих случаях:
Процесс установки приватного репозитория состоит из следующих шагов:
6.3.5.1. Установка менеджера репозиториев
В данном руководстве будет рассмотрен пример установки менеджера репозитория Sonatype Nexus OSS.
В операционной системе Microsoft Windows
замените
nexus-work=${bundleBasedir}/../sonatype-work/nexus
на
nexus-work=${bundleBasedir}/nexus/sonatype-work/content
nexus.bat install
С помощью Docker
Локальный репозиторий можно также легко установить с помощью Docker. См. инструкции на Docker Hub.
6.3.5.2. Настройка прокси-репозитория
Щелкните на ссылку Repositories в панели слева.
На открывшейся странице нажмите кнопку Add, затем выберите пункт Proxy Repository. Будет создан новый репозиторий. Заполните обязательные поля на вкладке Configuration:
Создайте группу репозиториев. В Nexus нажмите кнопку Add, затем выберите Repository Group и проделайте следующие шаги на вкладке Configuration:
Если у вас есть подписка на Премиум-дополнения, то создайте еще один репозиторий со следующими настройками:
6.3.5.3. Использование приватного репозитория
Теперь приватный репозиторий готов к работе. Найдите URL группы cuba-group в верхней части экрана, например:
http://localhost:8081/nexus/content/groups/cuba-group
Во время первой сборки проекта ваш новый репозиторий скачает артефакты и сохранит их в кэше для дальнейшего использования. Вы можете найти эти файлы в папке c:\nexus-2.14.3-02\sonatype-work.
6.3.5.4. Репозиторий в изолированной сети
Если вам требуется разработка приложения CUBA в сети без доступа к Интернет, то проделайте следующие шаги:
c:\nexus-2.14.3-02\sonatype-work
Если вам нужно добавить артефакты от новой платформы в изолированный репозиторий, вам потребуется вернуться в окружение, подключенное к интернету, затем выполнить сборку проекта через онлайн-репозиторий и скопировать содержимое кэша репозитория в изолированное окружение.
6.3.5.5. CUBA Studio в изолированной сети
Сейчас вы можете открыть CUBA Studio в изолированной сети:
Рекомендуемый способ создания нового проекта - использование CUBA Studio. Пример рассмотрен в главе Быстрый старт данного руководства.
Также можно легко создать проект в CUBA CLI:
После этого в текущем каталоге будет создан подкаталог с проектом.
После создания проекта вы можете продолжить разрабатывать его в Studio или CLI, либо создать файлы проекта для IntelliJ IDEA и открыть проект в IDE.
Для ведения логов в платформе используется фреймворк Logback.
Для вывода в лог используйте SLF4J API, получая логгер по имени текущего класса. Пример создания логгера и вывода в него:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Foo {
// create logger
private Logger log = LoggerFactory.getLogger(Foo.class);
private void someMethod() {
// output message with DEBUG level
log.debug("someMethod invoked");
}
}
Настройка логирования для блоков Middleware, Web Client и Web Portal производится на уровне сервера приложения - в варианте быстрого развертывания это Tomcat. Блок Desktop Client имеет самостоятельную настройку логирования.
6.5.1. Настройка логирования в Tomcat
В данном разделе рассматривается настройка логгирования на этапе разработки приложения.
При выполнении задачи Gradle setupTomcat в каталог проекта устанавливается сервер Tomcat, и производится его дополнительная конфигурация. В частности, в подкаталоге tomcat/bin создаются файлы setenv.bat и setenv.sh, а в подкаталоге tomcat/conf файл logback.xml.
Файлы setenv.* в переменной CATALINA_OPTS в числе прочего устанавливают параметры загрузки конфигурационного файла logback.xml.
Файл logback.xml определяет конфигурацию логирования. Рассмотрим структуру этого файла.
Для файлового аппендера в параметре file задается путь к файлу лога. По умолчанию это файл tomcat/logs/app.log.
Минимальный уровень указывается в атрибуте level. Например, если для логгера задан приоритет INFO, то сообщения с уровнями DEBUG и TRACE выводиться не будут. Следует иметь в виду, что на вывод сообщения также влияет порог уровня, заданный в аппендере.
Оперативно изменять уровни для логгеров и пороги аппендеров для работающего сервера можно с помощью экрана Administration > Server Log, доступного в веб-клиенте. Сделанные настройки логирования действуют только в текущем сеансе работы сервера и в файл не сохраняются. Этот экран позволяет также просматривать и загружать файлы логов из каталога журналов сервера tomcat/logs.
Платформа автоматически добавляет к сообщениям, выводимым в лог, следующую информацию:
Например, следующее сообщение в логе выведено кодом блока Middleware (app-core), работающим от имени пользователя admin:
16:12:20.498 DEBUG [http-nio-8080-exec-7/app-core/admin] com.haulmont.cuba.core.app.DataManagerBean - loadList: ...
6.5.2. Настройка логирования в десктоп-клиенте
Для десктоп клиента файл logback.xml должен находиться в каталоге исходников модуля desktop проекта. При сборке приложения он упаковывается в соответствующий JAR файл и доступен в CLASSPATH.
Для настройки логирования в своем проекте выполните следующее:
public class SampleApp extends App {
...
@Override
protected String getDefaultLogConfig() {
return "sample-logback.xml";
}
В данном разделе приведен список логгеров фреймворка, которые могут быть полезны при поиске проблем в приложении.
eclipselink.sql
При установке в DEBUG, EclipseLink ORM логгирует все SQL-операторы и время их выполнения. Данный логгер уже определен в стандартном logback.xml, поэтому достаточно только изменить его уровень. Например:
<configuration>
...
<logger name="eclipselink.sql" level="DEBUG"/>
Пример вывода в лог:
2018-09-21 12:48:18.583 DEBUG [http-nio-8080-exec-5/app-core/admin] com.haulmont.cuba.core.app.RdbmsStore - loadList: metaClass=sec$User, view=com.haulmont.cuba.security.entity.User/user.browse, query=select u from sec$User u, max=50
2018-09-21 12:48:18.586 DEBUG [http-nio-8080-exec-5/app-core/admin] eclipselink.sql - <t 891235430, conn 1084868057> SELECT t1.ID AS a1, t1.ACTIVE AS a2, t1.CHANGE_PASSWORD_AT_LOGON AS a3, t1.CREATE_TS AS a4, t1.CREATED_BY AS a5, t1.DELETE_TS AS a6, t1.DELETED_BY AS a7, t1.EMAIL AS a8, t1.FIRST_NAME AS a9, t1.IP_MASK AS a10, t1.LANGUAGE_ AS a11, t1.LAST_NAME AS a12, t1.LOGIN AS a13, t1.LOGIN_LC AS a14, t1.MIDDLE_NAME AS a15, t1.NAME AS a16, t1.PASSWORD AS a17, t1.POSITION_ AS a18, t1.TIME_ZONE AS a19, t1.TIME_ZONE_AUTO AS a20, t1.UPDATE_TS AS a21, t1.UPDATED_BY AS a22, t1.VERSION AS a23, t1.GROUP_ID AS a24, t0.ID AS a25, t0.DELETE_TS AS a26, t0.DELETED_BY AS a27, t0.NAME AS a28, t0.VERSION AS a29 FROM SEC_USER t1 LEFT OUTER JOIN SEC_GROUP t0 ON (t0.ID = t1.GROUP_ID) WHERE (t1.DELETE_TS IS NULL) LIMIT ? OFFSET ?
bind => [50, 0]
2018-09-21 12:48:18.587 DEBUG [http-nio-8080-exec-5/app-core/admin] eclipselink.sql - <t 891235430, conn 1084868057> [1 ms] spent
com.haulmont.cuba.core.sys.AbstractWebAppContextLoader
При установке в TRACE, фреймворк логгирует свойства приложения, заданные в файлах и полученные из компонентов приложения во время старта сервера, что может помочь в случае если сервер не запускается нормально.
Имейте в виду, что может понадобиться также установить порог TRACE для соответствующего appender, так как обычно они имеют более высокий порог. Например:
<configuration>
...
<appender name="File" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>TRACE</level>
</filter>
...
<logger name="com.haulmont.cuba.core.sys.AbstractWebAppContextLoader" level="TRACE"/>
Пример вывода в лог:
2018-09-21 12:38:59.525 TRACE [localhost-startStop-1] com.haulmont.cuba.core.sys.AbstractWebAppContextLoader - AppProperties of the 'core' block:
cuba.anonymousSessionId=9c91dbdf-3e73-428e-9088-d586da2434c5
cuba.automaticDatabaseUpdate=true
...
В данном разделе содержится информация об использовании пошаговой отладки CUBA-приложений.
Запустить сервер Tomcat в режиме отладки можно либо выполнением команды сборки
gradle start
либо запуском командного файла bin/debug.* установленного Tomcat.
После этого сервер будет принимать подключения отладчика на порту 8787. Порт можно изменить в файле bin/setenv.* в переменной JPDA_OPTS.
Для пошаговой отладки в Intellij IDEA необходимо в проекте приложения создать новый элемент Run/Debug Configuration типа Remote, и в его поле Port указать 8787.
6.6.2. Простая отладка в веб-браузере
Самый простой способ отладки ошибок на клиентской стороне без использования GWT Super Dev Mode - это использовать конфигурацию отладки внутри модуля web.
configure(webModule) {
configurations {
webcontent
debug // a new configuration
}
''''''
}
dependencies {
provided(servletApi)
compile(guiModule)
debug("com.haulmont.cuba:cuba-web-toolkit:$cubaVersion:debug@zip")
}
Если у вас подключено премиум-дополнение charts, используйте зависимость debug("com.haulmont.charts:charts-web-toolkit:$cubaVersion:debug@zip").
task deploy.doLast {
project.delete "$cuba.tomcat.dir/webapps/app/VAADIN/widgetsets"
project.copy {
from zipTree(configurations.debug.singleFile)
into "$cuba.tomcat.dir/webapps/app"
}
}
Сценарии отладки будут развёрнуты в папке $cuba.tomcat.dir/webapps/app/VAADIN/widgetsets/com.haulmont.cuba.web.toolkit.ui.WidgetSet.
6.6.3. Отладка виджетов в веб-браузере
Для отладки виджетов на стороне браузера можно использовать GWT Super Dev Mode.
gradlew debugWidgetSet
GWT Code Server будет перекомпилировать ваш widgetset при изменениях кода виджетов.
Модульные тесты (unit tests) можно создавать и выполнять и на уровне Middleware, и на клиентском уровне. Для этого платформа включает в себя фреймворки JUnit и JMockit.
Допустим, имеется следующий контроллер экрана:
public class OrderEditor extends AbstractEditor {
@Named("itemsTable.add")
protected AddAction addAction;
@Override
public void init(Map<String, Object> params) {
addAction.setWindowId("sales$Product.browse");
addAction.setHandler(new Lookup.Handler() {
@Override
public void handleLookup(Collection items) {
// some code
}
});
}
}
Тогда можно написать следующий тест, проверяющий работу метода init():
public class OrderEditorTest {
OrderEditor editor;
@Mocked
Window.Editor frame;
@Mocked
AddAction addAction;
@Before
public void setUp() throws Exception {
editor = new OrderEditor();
editor.setWrappedFrame(frame);
editor.addAction = addAction;
}
@Test
public void testInit() {
editor.init(Collections.<String, Object>emptyMap());
editor.setItem(new Order());
new Verifications() {
{
addAction.setWindowId("sales$Product.browse");
addAction.setHandler(withInstanceOf(Window.Lookup.Handler.class));
}
};
}
}
6.7.2. Интеграционные тесты Middleware
На уровне Middleware можно создавать интеграционные тесты, которые выполняются в полнофункциональном контейнере Spring с подключением к базе данных. В тестах такого типа можно выполнять код любого слоя внутри Middleware - от сервисов до ORM.
Для того, чтобы выполнять тесты из IDE, создайте каталог test в модуле core рядом с src. После этого пересоздайте проектные файлы IDE.
Платформа содержит класс TestContainer, который может быть использован в качестве базового для тестовых контейнеров приложения. Создайте наследника этого класса в каталоге test модуля core и в его конструкторе переопределите параметры загрузки компонентов и свойств приложения, а также параметры подключения к тестовой БД. Например:
public class SalesTestContainer extends TestContainer {
public SalesTestContainer() {
super();
appComponents = Arrays.asList(
"com.haulmont.cuba"
// add CUBA premium add-ons here
// "com.haulmont.bpm",
// "com.haulmont.charts",
// "com.haulmont.fts",
// "com.haulmont.reports",
// and custom app components if any
);
appPropertiesFiles = Arrays.asList(
// List the files defined in your web.xml
// in appPropertiesConfig context parameter of the core module
"com/company/sales/app.properties",
// Add this file which is located in CUBA and defines some properties
// specifically for test environment. You can replace it with your own
// or add another one in the end.
"test-app.properties",
"com/company/sales/test-app.properties");
dbDriver = "org.postgresql.Driver";
dbUrl = "jdbc:postgresql://localhost/sales_test";
dbUser = "cuba";
dbPassword = "cuba";
}
}
Пример собственного файлв test-app.properties:
cuba.webContextName = app-core
sales.someProperty = someValue
В качестве базы данных рекомендуется использовать отдельную тестовую БД, которую можно создавать, например, следующей задачей в build.gradle:
configure(coreModule) {
...
task createTestDb(dependsOn: assemble, description: 'Creates local Postgres database for tests', type: CubaDbCreation) {
dbms = 'postgres'
dbName = 'sales_test'
dbUser = 'cuba'
dbPassword = 'cuba'
}
Тестовый контейнер используется в классах тестов в качестве JUnit rule, указанного с помощью аннотации @ClassRule:
public class CustomerLoadTest {
@ClassRule
public static SalesTestContainer cont = SalesTestContainer.Common.INSTANCE;
private Customer customer;
@Before
public void setUp() throws Exception {
customer = cont.persistence().createTransaction().execute(em -> {
Customer customer = cont.metadata().create(Customer.class);
customer.setName("testCustomer");
em.persist(customer);
return customer;
});
}
@After
public void tearDown() throws Exception {
cont.deleteRecord(customer);
}
@Test
public void test() {
try (Transaction tx = cont.persistence().createTransaction()) {
EntityManager em = cont.persistence().getEntityManager();
TypedQuery<Customer> query = em.createQuery(
"select c from sales$Customer c", Customer.class);
List<Customer> list = query.getResultList();
tx.commit();
assertTrue(list.size() > 0);
}
}
}
В данном примере тестовый контейнер инициализируется один раз для всех тестовых методов класса, и уничтожается после того, как все они выполнены.
Так как запуск контейнера занимает некоторое время, имеет смысл инициализировать контейнер один раз для тестов из нескольких (или всех) классов. Для этого создайте общий синглтон-экземпляр тестового контейнера, например:
public class SalesTestContainer extends TestContainer {
public SalesTestContainer() {
...
}
public static class Common extends SalesTestContainer {
public static final SalesTestContainer.Common INSTANCE = new SalesTestContainer.Common();
private static volatile boolean initialized;
private Common() {
}
@Override
public void before() throws Throwable {
if (!initialized) {
super.before();
initialized = true;
}
setupContext();
}
@Override
public void after() {
cleanupContext();
// never stops - do not call super
}
}
}
И используйте его в тестовых классах:
public class CustomerLoadTest {
@ClassRule
public static SalesTestContainer cont = SalesTestContainer.Common.INSTANCE;
...
}
Полезные методы тестового контейнера
Класс TestContainer содержит следующие методы, которые можно использовать в коде тестов (см. пример CustomerLoadTest выше):
Логирование
Класс TestContainer настраивает логирование в соответствие с файлом test-logback.xml, предоставляемым платформой. Данный файл содержится в артефакте cuba-core-tests.
Для того, чтобы настроить уровни логирования в своих тестах, необходимо выполнить следующее:
public class MyTestContainer extends TestContainer {
static {
System.setProperty("logback.configurationFile", "my-test-logback.xml");
}
// ...
}
Если в вашем проекте используются дополнительные хранилища, необходимо создать соответствующие источники данных JDBC в тестовом контейнере. Например, если у вас есть хранилище mydb, являющееся базой данных PostgreSQL, добавьте следующий метод в класс тестового контейнера:
public class MyTestContainer extends TestContainer {
// ...
@Override
protected void initDataSources() {
super.initDataSources();
try {
Class.forName("org.postgresql.Driver");
TestDataSource mydbDataSource = new TestDataSource(
"jdbc:postgresql://localhost/mydatabase", "db_user", "db_password");
TestContext.getInstance().bind(
AppContext.getProperty("cuba.dataSourceJndiName_mydb"), mydbDataSource);
} catch (ClassNotFoundException | NamingException e) {
throw new RuntimeException("Error initializing datasource", e);
}
}
}
Кроме того, если тип дополнительной базы данных отличается от основной, необходимо добавить ее драйвер как testRuntime зависимость модуля core в build.gradle, например:
configure(coreModule) {
// ...
dependencies {
// ...
testRuntime(hsql)
jdbc('org.postgresql:postgresql:9.4.1212')
testRuntime('org.postgresql:postgresql:9.4.1212') // add this
}
6.7.3. Интеграционные тесты клиентского уровня
Интеграционные тесты на клиентском уровне реализуются с применением фреймворка JMockit. С его помощью тест изолируется от Middleware, а также создаются необходимые объекты инфраструктуры.
Класс клиентского интеграционного теста должен быть унаследован от CubaClientTestCase. В методе @Before необходимо вызвать унаследованные методы addEntityPackage(), setViewConfig() и затем setupInfrastructure() для создания объектов Metadata и Configuration и развертывания метаданных по выбранным сущностям. Далее в методе @Before можно дополнить инфраструктуру необходимыми мок-объектами с помощью конструкции Expectations или NonStrictExpectations.
Пример инициализирующего метода @Before одного из тестов платформы:
@Before
public void setUp() throws Exception {
addEntityPackage("com.haulmont.cuba.security.entity");
addEntityPackage("com.haulmont.cuba.core.entity");
addEntityPackage("com.haulmont.cuba.gui.data.impl.testmodel1");
setViewConfig("/com/haulmont/cuba/gui/data/impl/testmodel1/test-views.xml");
setupInfrastructure();
metadataSession = metadata.getSession();
dataSupplier = new TestDataSupplier();
dataSupplier.commitCount = 0;
new NonStrictExpectations() {
@Mocked ClientConfig clientConfig;
@Mocked PersistenceHelper persistenceHelper;
{
configuration.getConfig(ClientConfig.class); result = clientConfig;
clientConfig.getCollectionDatasourceDbSortEnabled(); result = true;
persistenceManager.getMaxFetchUI(anyString); result = 10000;
PersistenceHelper.isNew(any); result = false;
}
};
}
Платформа CUBA поддерживает технологию Hot Deploy, которая позволяет мгновенно отображать сделанные в проекте изменения в работающем приложении без необходимости перезапускать сервер. Принцип работы Hot deploy заключается во временном копировании изменённых ресурсов и исходных файлов Java проекта в конфигурационный каталог приложения, откуда они загружаются и компилируются работающим приложением.
Как это работает
Когда в каком-то файле исходного кода производятся изменения, Studio копирует этот файл в конфигурационный каталог веб-приложения (tomcat/conf/app или tomcat/conf/app-core). Ресурсы в конфигурационном каталоге имеют приоритет над ресурсами в JAR-файлах приложения, поэтому работающее приложение загрузит именно эти ресурсы, когда они понадобятся. Если загружается файл исходного кода на Java, то приложение компилирует его на лету и загружает результирующий класс.
Кроме того, Studio посылает приложению специальные сигналы для того чтобы заставить его очистить кэши и перезагрузить измененные ресурсы. Это кэш локализованных сообщений и конфигурации представлений, зарегистрированных экранов и меню.
При перезагрузке сервера приложения все файлы в конфигурационном каталоге удаляются, и JAR-файлы содержат последние версии вашего кода.
Какие изменения применяются через hot deploy
Изменения в прочих классах и бинах UI и среднего слоя, включая их статические методы, могут применяться на лету только тогда, когда изменяется ещё хотя бы один файл экрана или реализации сервиса, который их используют.
Причина такого поведения в том, что перезагрузка классов вызывается только по сигналу: для контроллера экрана это переоткрытие этого экрана пользователем, а для сервисов Studio генерирует особый файл-триггер, который будет распознан сервером и использован для перезагрузки конкретного класса сервиса и всех его зависимостей.
Какие изменения не применяются через hot deploy
Использование hot deploy в Studio
Настройки Hot deploy можно изменить в Studio на странице Help > Settings:
Если мгновенный hot deploy отключен, применение изменений можно вызвать вручную командой меню Run > Hot deploy conf.
В данной главе рассматриваются различные аспекты развертывания и эксплуатации CUBA-приложений.
На диаграмме ниже приведена возможная структура развернутого приложения. В приведенном варианте приложение обеспечивает отсутствие единой точки отказа, балансировку нагрузки и подключение различных типов клиентов.
В простейшем случае, однако, приложение может быть установлено на одном компьютере, содержащем, в том числе, и базу данных. Различные варианты развертывания в зависимости от нагрузки и требований к отказоустойчивости подробно рассмотрены в Масштабирование приложения.
7.1. Домашний каталог приложения
Домашний каталог - это каталог файловой системы, в котором могут размещаться Каталоги приложения. Он используется во всех вариантах развертывания кроме быстрого развертывания в Tomcat. В последнем случае каталоги приложения располагаются в специфичных каталогах Tomcat.
Домашний каталог формируется просто путем указания общего корня для каталогов приложения. Обычно это делается в файле /WEB-INF/local.app.properties внутри WAR или UberJAR.
Пример указания домашнего каталога в runtime:
task buildWar(type: CubaWarBuilding) {
appHome = '${app.home}'
// ...
}
cuba.logDir = ${app.home}/logs
cuba.confDir = ${app.home}/${cuba.webContextName}/conf
cuba.tempDir = ${app.home}/${cuba.webContextName}/temp
cuba.dataDir = ${app.home}/${cuba.webContextName}/work
...
java -Dapp.home=/opt/app_home ...
Способ указания системных свойств Java зависит от используемого сервера приложения. В случае Tomcat рекомендуется задавать их в файле bin/setenv.sh (или bin/setenv.bat).
/opt/app_home/
app/
conf/
temp/
work/
app-core/
conf/
temp/
work/
logs/
java -Dapp.home=/opt/app_home -jar app.jar
В данном разделе описываются каталоги файловой системы, используемые различными блоками приложения во время выполнения.
7.2.1. Конфигурационный каталог
Каталог конфигурации предназначен для размещения ресурсов, дополняющих и переопределяющих конфигурацию, пользовательский интерфейс и бизнес-логику после развертывания приложения. Переопределение обеспечивается механизмом загрузки интерфейса инфраструктуры Resources, который сначала выполняет поиск в конфигурационном каталоге, а потом в classpath, так что одноименные ресурсы в конфигурационном каталоге имеют приоритет над расположенными в JAR-файлах и каталогах классов.
Конфигурационный каталог может содержать следующие типы ресурсов:
Расположение конфигурационного каталога определяется свойством приложения cuba.confDir. В случае быстрого развертывания в Tomcat это подкаталог с именем веб-приложения в каталоге tomcat/conf, например tomcat/conf/app-core для Middleware. Для других вариантов развертывания конфигурационный каталог размещается внутри домашнего каталога приложения.
Рабочий каталог используется приложением для хранения файлов данных и конфигурации.
Например, подкаталог filestorage рабочего каталога по умолчанию используется хранилищем загруженных файлов. Кроме того, блок Middleware на старте сохраняет в рабочем каталоге сгенерированные файлы persistence.xml и orm.xml.
Расположение рабочего каталога определяется свойством приложения cuba.dataDir. В случае быстрого развертывания в Tomcat это подкаталог с именем веб-приложения в каталоге tomcat/work. Для других вариантов развертывания рабочий каталог размещается внутри домашнего каталога приложения.
Состав и настройка файлов журналов определяются конфигурацией фреймворка Logback. По умолчанию в приложении используется файл конфигурации logback.xml, предоставляемый платформой в корне classpath. В соответствии с его настройками, вывод лога осуществляется в standard output.
Для того чтобы указать собственную конфигурацию, необходимо передать системное свойство Java logback.configurationFile с путем к файлу конфигурации. В разделе Настройка логирования в Tomcat приведена информации по настройке в случае быстрого развертывания.
Содержимое logback.xml определяет, где будут расположены файлы логов. Это может быть подкаталог внутри некоторого каталога Tomcat (tomcat/logs в случае быстрого развертывания), или подкаталог домашнего каталога приложения. Управлять расположением можно, если взять logback.xml из каталога deploy/tomcat/conf проекта и изменить свойство logDir, например:
<configuration debug="false">
<property name="logDir" value="${app.home}/logs"/>
<!-- ... -->
Приложение должно знать, где расположены файлы логов, для того, чтобы позволить администраторам просматривать и загружать их в экране Administration > Server Log. Поэтому установите свойство приложения cuba.logDir в тот же каталог, который задается в logback.xml.
См. также Логирование.
Данный каталог может быть использован для создания произвольных временных файлов во время выполнения приложения. Путь к временному каталогу определяется свойством приложения cuba.tempDir. В случае быстрого развертывания в Tomcat это подкаталог с именем веб-приложения в каталоге tomcat/temp. Для других вариантов развертывания временный каталог размещается внутри домашнего каталога приложения.
7.2.5. Каталог скриптов базы данных
В данном каталоге развернутого блока Middleware хранится набор SQL скриптов создания и обновления БД.
Структура каталога скриптов повторяет описанную в разделе Скрипты создания и обновления БД, но имеет один дополнительный верхний уровень, разделяющий скрипты используемых компонентов и самого приложения. Нумерация каталогов верхнего уровня определяется во время сборки проекта.
Расположение каталога скриптов БД определяется свойством приложения cuba.dbDir. В варианте быстрого развертывания в Tomcat это подкаталог WEB-INF/db каталога веб-приложения среднего слоя: tomcat/webapps/app-core/WEB-INF/db. Для других вариантов развертывания скрипты размещаются в каталоге /WEB-INF/db внутри WAR или UberJAR.
В данном разделе рассматриваются различные варианты развертывания CUBA-приложений:
7.3.1. Быстрое развертывание в Tomcat
Быстрое развертывание используется по умолчанию при разработке приложения, так как обеспечивает минимальное время сборки, установки и старта приложения. Данный вариант может также использоваться и для эксплуатации приложения.
Быстрое развертывание производится с помощью задачи deploy, объявленной для модулей core и web в файле build.gradle. Перед первым выполнением deploy необходимо установить и проинициализировать локальный сервер Tomcat с помощью задачи setupTomcat.
|
Пожалуйста, убедитесь, что ваше окружение не содержит переменных CATALINA_HOME, CATALINA_BASE и CLASSPATH. Эти переменные могут вызвать проблемы с запуском сервера, которые не будут отражаться в логах. Перезагрузите компьютер после удаления переменных. |
В результате быстрого развертывания в каталоге, задаваемом свойством ext.tomcatDir скрипта build.gradle создается следующая структура (перечислены только важные каталоги и файлы, описанные ниже):
bin/
setenv.bat, setenv.sh
startup.bat, startup.sh
debug.bat, debug.sh
shutdown.bat, shutdown.sh
conf/
catalina.properties
server.xml
logback.xml
logging.properties
Catalina/
localhost/
app/
app-core/
lib/
hsqldb-2.2.9.jar
logs/
app.log
shared/
lib/
temp/
app/
app-core/
webapps/
app/
app-core/
work/
app/
app-core/
|
Если вы столкнулись с медленной загрузкой Tomcat под Linux, установленной на виртуальной машине (VPS), попробуйте настроить JVM на использование источника неблокирующей энтропии в файле setenv.sh: CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom" |
Для запуска сервера в текущем консольном окне вместо startup.* используйте команды
> catalina.bat run
$ ./catalina.sh run
shared.loader=${catalina.home}/shared/lib/*.jar
Дескриптор развертывания на уровне сервера должен иметь имя приложения и расширение .xml. То есть для создания такого дескриптора, например, для приложения app-core, необходимо скопировать содержимое файла webapps/app-core/META-INF/context.xml в файл conf/Catalina/localhost/app-core.xml.
Задачи deploy файла сборки копируют в этот каталог все библиотеки, не перечисленные в параметре jarNames, то есть не специфичные для данного приложения.
Задачи deploy файла сборки создают подкаталоги приложений с именами, указанными в параметрах appName, и кроме прочего копируют в их подкаталоги WEB-INF/lib библиотеки, перечисленные в параметре jarNames.
7.3.1.1. Использование Tomcat при эксплуатации приложения
Процедура быстрого развертывания по умолчанию создает веб приложения app и app-core, работающие на локальном инстансе Tomcat на порту 8080. Это означает, что веб-клиент доступен по адресу http://localhost:8080/app.
Вы можете использовать этот экземпляр Tomcat для эксплуатации приложения, просто скопировав его на сервер. После этого необходимо установить имя хоста сервера в файлах conf/app/local.app.properties и conf/app-core/local.app.properties (создайте файлы если они не существуют):
cuba.webHostName = myserver
cuba.webAppUrl = http://myserver:8080/app
Кроме того, необходимо настроить подключение к production базе данных. Это можно сделать в файле context.xml веб-приложения (webapps/app-core/META-INF/context.xml), или скопировать этот файл в conf/Catalina/localhost/app-core.xml как описано в предыдущем разделе, чтобы разделить настройки соединения с БД для разработки и эксплуатации.
Базу данных для production можно создать из бэкапа той базы, которая использовалась при разработке, либо настроить автоматическое создание и обновление БД. См. Создание и обновление БД при эксплуатации приложения.
Опциональная конфигурация
tomcat/
conf/
ROOT/
local.app.properties
app-core/
local.app.properties
webapps/
ROOT/
app-core/
и используйте / в качестве веб контекста в файле conf/ROOT/local.app.properties:
cuba.webContextName = /
7.3.2. Развертывание WAR в Jetty
Рассмотрим пример сборки WAR-файлов и их развертывания на сервере Jetty. Предполагается, что приложение использует СУБД PostgreSQL.
task buildWar(type: CubaWarBuilding) {
appHome = '${app.home}'
appProperties = ['cuba.automaticDatabaseUpdate': 'true']
singleWar = false
}
В данном случае собирается два WAR-файла, отдельно для блоков Middleware и Web Client.
gradlew buildWar
В результате в подкаталоге build\distributions\war проекта будут созданы файлы app-core.war и app.war.
java -jar c:\work\jetty-home\start.jar --add-to-start=http,jndi,deploy,plus,ext,resources
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
<Arg></Arg>
<Arg>jdbc/CubaDS</Arg>
<Arg>
<New class="org.postgresql.ds.PGSimpleDataSource">
<Set name="ServerName">localhost</Set>
<Set name="PortNumber">5432</Set>
<Set name="DatabaseName">test</Set>
<Set name="User">cuba</Set>
<Set name="Password">cuba</Set>
</New>
</Arg>
</New>
</Configure>
Файл app-jetty.xml для MS SQL должен соответствовать следующему шаблону:
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id='wac' class="org.eclipse.jetty.webapp.WebAppContext">
<New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
<Arg/>
<Arg>jdbc/CubaDS</Arg>
<Arg>
<New class="org.apache.commons.dbcp2.BasicDataSource">
<Set name="driverClassName">com.microsoft.sqlserver.jdbc.SQLServerDriver</Set>
<Set name="url">jdbc:sqlserver://server_name;databaseName=db_name</Set>
<Set name="username">username</Set>
<Set name="password">password</Set>
<Set name="maxIdle">2</Set>
<Set name="maxTotal">20</Set>
<Set name="maxWaitMillis">5000</Set>
</New>
</Arg>
</New>
</Configure>
commons-pool2-2.4.2.jar
commons-dbcp2-2.1.1.jar
commons-logging-1.2.jar
--exec
-Xdebug
-agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=n
-Dapp.home=c:\work\app_home
-Dlogback.configurationFile=c:\work\app_home\logback.xml
# ---------------------------------------
app-jetty.xml
java -jar c:\work\jetty-home\start.jar
7.3.3. Развертывание WAR в WildFly
WAR-файлы с приложением CUBA можно разворачивать на сервере WildFly. Рассмотрим пример сборки WAR-файлов для приложения, использующего PostgreSQL 9.6, и их развертывания на сервере WildFly версии 14.0.0 под Windows.
<property name="logDir" value="${app.home}/logs"/>
set "JAVA_OPTS=%JAVA_OPTS% -Dapp.home=%USERPROFILE%/app_home -Dlogback.configurationFile=%USERPROFILE%/app_home/logback.xml"
Здесь мы задаём системное свойство app.home, содержащее домашний каталог приложения, и указываем, где находится конфигурационный файл logback.xml. Вместо переменной %USERPROFILE% можно использовать абсолютный путь.
<subsystem xmlns="urn:jboss:domain:logging:6.0">
<add-logging-api-dependencies value="false"/>
<use-deployment-logging-config value="false"/>
. . .
</subsystem>
Name: Cuba
JNDI Name: java:/jdbc/CubaDS
JDBC Driver: postgresql-9.4.1212.jar
Driver Module Name: org.postgresql
Driver Class Name: org.postgresql.Driver
Connection URL: URL вашей БД
Username: имя пользователя БД
Password: пароль БД
Драйвер JDBC будет доступен в списке обнаруженных драйверов, если вы скопировали файл postgresql-x.y.z.jar на предыдущем шаге.
Выполните проверку соединения, нажав кнопку Test connection.
task buildWar(type: CubaWarBuilding) {
appProperties = ['cuba.automaticDatabaseUpdate' : true]
singleWar = false
appHome = '${app.home}'
doAfter = {
copy {
from 'jboss-deployment-structure.xml'
into "${project.buildDir}/buildWar/core/war/META-INF/"
}
copy {
from 'jboss-deployment-structure.xml'
into "${project.buildDir}/buildWar/web/war/META-INF/"
}
}
}
|
Для конфигурации singleWAR задача будет отличаться: task buildWar(type: CubaWarBuilding) { webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml' appProperties = ['cuba.automaticDatabaseUpdate' : true] appHome = '${app.home}' doAfter = { copy { from 'jboss-deployment-structure.xml' into "${project.buildDir}/buildWar/war/META-INF/" } } } Если ваш проект также содержит модуль Polymer, нужно добавить в файл single-war-web.xml следующую конфигурацию: <servlet> <servlet-name>default</servlet-name> <init-param> <param-name>resolve-against-context-root</param-name> <param-value>true</param-value> </init-param> </servlet> |
<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.0">
<deployment>
<exclusions>
<module name="org.apache.commons.logging" />
<module name="org.apache.log4j" />
<module name="org.jboss.logging" />
<module name="org.jboss.logging.jul-to-slf4j-stub" />
<module name="org.jboss.logmanager" />
<module name="org.jboss.logmanager.log4j" />
<module name="org.slf4j" />
<module name="org.slf4j.impl" />
<module name="org.slf4j.jcl-over-slf4j" />
</exclusions>
</deployment>
</jboss-deployment-structure>
7.3.4. Развертывание WAR в Tomcat Windows Service
task buildWar(type: CubaWarBuilding) {
appHome = './app_home'
singleWar = false
includeContextXml = true
includeJdbcDriver = true
appProperties = ['cuba.automaticDatabaseUpdate': true]
}
Если параметры сервера отличаются от параметров локального Tomcat, используемого для быстрого развертывания, укажите соответствующие свойства приложения:
task buildWar(type: CubaWarBuilding) {
appHome = './app_home'
singleWar = false
includeContextXml = true
includeJdbcDriver = true
appProperties = [
'cuba.automaticDatabaseUpdate': true,
'cuba.webPort': 9999,
'cuba.connectionUrlList': 'http://localhost:9999/app-core'
]
}
Можно также указать отдельный context.xml для настройки соединения с production БД, например:
task buildWar(type: CubaWarBuilding) {
appHome = './app_home'
singleWar = false
includeContextXml = true
includeJdbcDriver = true
appProperties = ['cuba.automaticDatabaseUpdate': true]
coreContextXmlPath = 'modules/core/web/META-INF/production-context.xml'
}
gradlew buildWar
UberJAR - это простейший способ запустить приложение CUBA в режиме эксплуатации. Вы собираете единый all-in-one JAR-файл с помощью задачи Gradle buildUberJar (см. также вкладку Deployment settings > Uber JAR в Studio) и запускаете приложение из командной строки, используя команду java:
java -jar app.jar
Все параметры приложения определяются во время сборки, но могут быть переопределены при запуске (см. ниже). Порт веб-приложения по умолчанию - 8080, и оно доступно по адресу http://host:8080/app. Если в проекте есть Polymer UI, он будет доступен по умолчанию по адресу http://host:8080/app-front.
Можно также собрать отдельные JAR файлы для Middleware и Web Client, и запустить их аналогично:
java -jar app-core.jar
java -jar app.jar
Порт веб-клиента по умолчанию - 8080, он будет подключаться к middleware, использующему localhost:8079. Таким образом, выполнив эти две команды в двух разных терминалах Windows, вы сможете подключиться к веб-клиенту приложения по адресу http://localhost:8080/app.
Вы можете изменить параметры, определяемые во время сборки, передав свойства приложения через системные свойства Java. Кроме того, порты, имена контекстов и пути к конфигурационным файлам можно передавать в качестве аргументов командной строки.
Аргументы командной строки
java -jar app.jar -port 9090
Следует учесть, что при указании порта для блока core необходимо также задать свойство приложения cuba.connectionUrlList для клиентских блоков, указав соответствующие адрес Middleware:
java -jar app-core.jar -port 7070
java -Dcuba.connectionUrlList=http://localhost:7070/app-core -jar app.jar
java -jar app.jar -contextName sales
Домашний каталог приложения
По умолчанию домашним каталогом является рабочий каталог приложения. Это означает, что каталоги приложения будут созданы в каталоге, из которого приложение запущено. Домашний каталог может быть также указан в системном свойстве app.home. Например, чтобы иметь домашний каталог в /opt/app_home, необходимо указать следующее в командной строке:
java -Dapp.home=/opt/app_home -jar app.jar
Логирование
Если необходимо изменить настройки логирования, заданные внутри JAR, то можно передать системное свойство Java logback.configurationFile с URL для загрузки внешнего конфигурационного файла, например:
java -Dlogback.configurationFile=file:./logback.xml -jar app.jar
Здесь подразумевается, что файл logback.xml расположен в папке, из которой запускается приложение.
Для правильного задания каталога логов убедитесь, что свойство logDir в logback.xml указывает на подкаталог logs домашнего каталога приложения:
<configuration debug="false">
<property name="logDir" value="${app.home}/logs"/>
<!-- ... -->
Остановка приложения
Корректно остановить приложение можно следующими способами:
Например:
# Start application 1 and listen to SHUTDOWN key on port 9090
java -jar app.jar -stopPort 9090
# Start application 2 and listen to MYKEY key on port 9090
java -jar app.jar -stopPort 9090 -stopKey MYKEY
# Shutdown application 1
java -jar app.jar -stop -stopPort 9090
# Shutdown application 2
java -jar app.jar -stop -stopPort 9090 -stopKey MYKEY
7.3.5.1. Настройка HTTPS для UberJAR
Ниже приведен пример настройки HTTPS с самоподписанным сертификатом для развертывания UberJAR.
keytool -keystore keystore.jks -alias jetty -genkey -keyalg RSA
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.server.ServerConnector">
<Arg name="server">
<Ref refid="Server"/>
</Arg>
<Set name="port">8090</Set>
</New>
</Arg>
</Call>
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.server.ServerConnector">
<Arg name="server">
<Ref refid="Server"/>
</Arg>
<Arg>
<New class="org.eclipse.jetty.util.ssl.SslContextFactory">
<Set name="keyStorePath">keystore.jks</Set>
<Set name="keyStorePassword">password</Set>
<Set name="keyManagerPassword">password</Set>
<Set name="trustStorePath">keystore.jks</Set>
<Set name="trustStorePassword">password</Set>
</New>
</Arg>
<Set name="port">8443</Set>
</New>
</Arg>
</Call>
</Configure>
Удостоверьтесь, что значения свойств keyStorePassword, keyManagerPassword и trustStorePassword совпадают с паролями, установленными в Keytool.
task buildUberJar(type: CubaUberJarBuilding) {
singleJar = true
coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml'
appProperties = ['cuba.automaticDatabaseUpdate' : true]
webJettyConfPath = 'jetty.xml'
}
Теперь приложение доступно по адресу https://localhost:8443/app.
7.3.6. Развертывание в облаке Jelastic
CUBA Studio позволяет легко развернуть приложение в облаке Jelastic.
|
В данный момент развертывание в облаке возможно только для проектов, использующих в качестве сервера базы данных PostgreSQL или HSQL. |
7.3.7. Развёртывание в облаке Bluemix
С помощью CUBA Studio можно легко развернуть приложение в облаке IBM® Bluemix®.
|
Развёртывание в облаке Bluemix в настоящее время рекомендуется только для проектов, использующих базу данных PostgreSQL. HSQLDB доступна только с опцией in-process, таким образом, база данных будет пересоздаваться каждый раз при перезапуске облачного приложения, соответственно, пользовательские данные будут потеряны. |
Используйте параметры из строки uri сервиса, как в примере ниже:
{
"elephantsql": [
{
"credentials": {
"uri": "postgres://ixbtsvsq:F_KyeQjpEdpQfd4n0KpEFCYyzKAbN1W9@qdjjtnkv.db.elephantsql.com:5432/ixbtsvsq",
"max_conns": "5"
}
}
]
}
Database user: ldwpelpl
Database password: eFwXx6lNFLheO5maP9iRbS77Sk1VGO_T
Database URL: echo-01.db.elephantsql.com:5432
Database name: ldwpelpl
В результате, в папке проекта build/distributions/war/ появился файл app.war.
applications:
- path: build/distributions/war/app.war
memory: 1G
instances: 1
domain: eu-gb.mybluemix.net
name: myluckycuba
host: myluckycuba
disk_quota: 1024M
buildpack: java_buildpack
env:
JBP_CONFIG_TOMCAT: '{tomcat: { version: 8.0.+ }}'
JBP_CONFIG_OPEN_JDK_JRE: '{jre: { version: 1.8.0_+ }}'
где
cd your_project_directory
bluemix api https://api.ng.bluemix.net
cf login
cf push
Команда push использует параметры, указанные в конфигурационном файле manifest.yml.
cf logs cuba-app --recent
7.3.8. Развертывание в облаке Heroku
Данный раздел описывает порядок развертывания приложения CUBA в облаке Heroku®.
|
Это руководство охватывает процесс развертывания проекта с использованием базы данных PostgreSQL. |
7.3.8.1. Развертывание WAR-файла в Heroku
Учетная запись Heroku
Создайте учетную запись в Heroku с помощью веб-браузера, будет достаточно бесплатного аккаунта hobby-dev. Затем войдите в аккаунт и создайте новое приложение с помощью кнопки New в верхней части страницы.
Задайте уникальное имя приложения (либо оставьте поле пустым, чтобы имя назначилось автоматически) и выберите подходящее геоположение сервера. Вы зарегистрировали приложение, например morning-beach-4895, это будет название приложения Heroku.
Сначала вас переадресует на вкладку Deploy. Выберите там метод развертывания Heroku Git.
Командная строка Heroku (CLI)
heroku login
heroku plugins:install heroku-cli-deploy
База данных PostgreSQL
С помощью браузера пройдите на страницу Heroku data
Вы можете использовать существующую базу Postgres или создать новую. Далее описываются шаги по созданию новой БД.
Как вариант, вы можете установить PostgreSQL с помощью Heroku CLI:
heroku addons:create heroku-postgresql:hobby-dev --app morning-beach-4895
Здесь morning-beach-4895 это название вашего приложения Heroku.
Теперь вы можете увидеть новую БД на вкладке Resources. База соединена с приложением Heroku. Чтобы получить детали для подключения к сервису БД, перейдите на страницу Datasource вашей БД в Heroku, опуститесь вниз до секции Administration и нажмите кнопку View credentials.
Host compute.amazonaws.com
Database d2tk
User nmmd
Port 5432
Password 9c05
URI postgres://nmmd:9c05@compute.amazonaws.com:5432/d2tk
Настройки проекта перед развертыванием
[
'cuba.automaticDatabaseUpdate' : true
]
Сборка WAR-файла
Соберите WAR-файл, выполнив команду buildWar в Gradle. Вы можете сделать это прямо в Студии, открыв диалог Search, или из командной строки:
gradlew buildWar
Проект CUBA использует Gradle wrapper (gradlew). Чтобы иметь возможность работать с командой gradlew, заранее создайте Gradle wrapper, использовав команду меню Build > Create or update Gradle wrapper.
Настройка приложения
web: java $JAVA_OPTS -cp webapp-runner.jar:libs/* webapp.runner.launch.Main --enable-naming --port $PORT build/distributions/war/app.war
Настройка Git
Откройте командную строку в папке $PROJECT_FOLDER и запустите команды, указанные ниже:
git init
heroku git:remote -a morning-beach-4895
git add .
git commit -am "Initial commit"
Развертывание приложения
Откройте командную строку в папке $PROJECT_FOLDER и запустите команды, указанные ниже:
Для *nix:
heroku jar:deploy webapp-runner.jar --includes libs/tomcat-dbcp.jar:build/distributions/war/app.war --app morning-beach-4895
Для Windows:
heroku jar:deploy webapp-runner.jar --includes libs\tomcat-dbcp.jar;build\distributions\war\app.war --app morning-beach-4895
Откройте вкладку Resources в панели управления Heroku. Должна появиться новая запись Dyno с командой из вашего Procfile:
Приложение в данный момент разворачивается. Вы можете отслеживать процесс по логам.
Мониторинг логов
Дождитесь сообщения в командной строке https://morning-beach-4895.herokuapp.com/ deployed to Heroku.
Чтобы отслеживать данные в логах, запустите в командной строке из любой папки следующую команду:
heroku logs --tail --morning-beach-4895
После завершения процесса развертывания ваше приложение будет доступно в браузере по ссылке https://morning-beach-4895.herokuapp.com
Вы также можете открыть приложение с помощью кнопки Open app, расположенной на панели Heroku.
7.3.8.2. Развертывание из GitHub в Heroku
Данное руководство описывает процесс настройки сборки и развертывания проекта, размещенного на GitHub.
Учетная запись Heroku
Создайте учетную запись в Heroku с помощью веб-браузера, будет достаточно бесплатного аккаунта hobby-dev. Затем войдите в аккаунт и создайте новое приложение с помощью кнопки New в верхней части страницы.
Задайте уникальное имя приложения (либо оставьте поле пустым, чтобы имя назначилось автоматически) и выберите подходящее геоположение сервера. Вы зарегистрировали приложение, например space-sheep-02453, это будет название приложения Heroku.
Сначала вас переадресует на вкладку Deploy. Выберите там метод развертывания GitHub. Следуйте инструкциям на экране, чтобы авторизоваться в учетную запись GitHub. Нажмите кнопку Search, чтобы вывести список доступных репозиториев GitHub вашей учетной записи, затем подключите желаемый репозиторий к проекту CUBA. Когда приложение Heroku подсоединено к GitHub, то вам доступна функция автоматического развертывания приложения Automatic Deploys. Это позволяет развертывать приложение в Heroku автоматически при каждом событии git push. В этом руководстве данная опция включена.
Командная строка Heroku (CLI)
heroku login
База данных PostgreSQL
Как вариант, вы можете установить PostgreSQL с помощью Heroku CLI, где space-sheep-02453 - это имя вашего Heroku приложения:
heroku addons:create heroku-postgresql:hobby-dev --app space-sheep-02453
Теперь вы можете увидеть новую БД на вкладке Resources. База соединена с приложением Heroku. Чтобы получить детали для подключения к сервису БД, перейдите на страницу Datasource вашей БД в Heroku, опуститесь вниз до секции Administration и нажмите кнопку View credentials.
Host compute.amazonaws.com
Database zodt
User artd
Port 5432
Password 367f
URI postgres://artd:367f@compute.amazonaws.com:5432/zodt
Настройки проекта перед развертыванием
<Context>
<Resource driverClassName="org.postgresql.Driver"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
name="jdbc/CubaDS"
password="367f"
type="javax.sql.DataSource"
url="jdbc:postgresql://compute.amazonaws.com/zodt"
username="artd"/>
<Manager pathname=""/>
</Context>
Настройка сборки
Добавьте следующую задачу Gradle в ваш файл $PROJECT_FOLDER/build.gradle
task stage(dependsOn: ['setupTomcat', ':app-core:deploy', ':app-web:deploy']) {
doLast {
// replace context.xml with heroku-context.xml
def src = new File('modules/core/web/META-INF/heroku-context.xml')
def dst = new File('deploy/tomcat/webapps/app-core/META-INF/context.xml')
dst.delete()
dst << src.text
// change port from 8080 to heroku $PORT
def file = new File('deploy/tomcat/conf/server.xml')
file.text = file.text.replace('8080', '${port.http}')
// add local.app.properties for core application
def coreConfDir = new File('deploy/tomcat/conf/app-core/')
coreConfDir.mkdirs()
def coreProperties = new File(coreConfDir, 'local.app.properties')
coreProperties.text = ''' cuba.automaticDatabaseUpdate = true '''
// rename deploy/tomcat/webapps/app to deploy/tomcat/webapps/ROOT
def rootFolder = new File('deploy/tomcat/webapps/ROOT')
if (rootFolder.exists()) {
rootFolder.deleteDir()
}
def webAppDir = new File('deploy/tomcat/webapps/app')
webAppDir.renameTo( new File(rootFolder.path) )
// add local.app.properties for web application
def webConfDir = new File('deploy/tomcat/conf/ROOT/')
webConfDir.mkdirs()
def webProperties = new File(webConfDir, 'local.app.properties')
webProperties.text = ''' cuba.webContextName = / '''
}
}
Procfile
Команда, которая запускает приложение в Heroku, передается через специальный файл Procfile. Создайте файл с названием Procfile в папке $PROJECT_FOLDER, содержащий следующий текст:
web: cd ./deploy/tomcat/bin && export 'JAVA_OPTS=-Dport.http=$PORT' && ./catalina.sh run
Это передает значение переменной среды JAVA_OPTS в Tomcat, который в свою очередь запускает скрипт Catalina.
Премиум дополнения
Если ваш проект использует премиальные дополнения CUBA, то укажите дополнительные переменные в приложении Heroku.
CUBA_PREMIUIM_USER | username
CUBA_PREMIUM_PASSWORD | password
Gradle wrapper
Проект CUBA использует Gradle wrapper (gradlew). Чтобы иметь возможность работать с командой gradlew, заранее создайте Gradle wrapper, использовав команду меню Build > Create or update Gradle wrapper.
java.runtime.version=1.8.0_121
git add gradlew gradlew.bat gradle/* system.properties Procfile
git commit -am "Added Gradle wrapper and Procfile"
Развертывание приложения
Как только вы выполните Push изменений в GitHub, то Heroku начнет разворачивать приложение.
git push
Контроль процесса развертывания осуществляется в панели Heroku на вкладке Activity. Перейдите по ссылке View build log, чтобы отслеживать лог.
После завершения процесса развертывания ваше приложение будет доступно в браузере по ссылке https://space-sheep-02453.herokuapp.com/
Вы также можете открыть приложение с помощью кнопки Open app, расположенной на панели Heroku.
Мониторинг логов
Чтобы отслеживать данные в логах, запустите в командной строке следующую команду:
heroku logs --tail --app space-sheep-02453
Логи Tomcat также доступны в веб-приложении: Menu > Administration > Server Log
Данный раздел описывает порядок развертывания приложения CUBA в Docker®.
|
Это руководство охватывает процесс развертывания проекта с использованием базы данных PostgreSQL. |
UberJAR - это простейший способ запустить приложение CUBA в режиме эксплуатации. Вы собираете единый all-in-one JAR-файл с помощью задачи Gradle buildUberJar (см. также вкладку Deployment settings > Uber JAR в Studio) и запускаете приложение из командной строки, используя команду java. Все параметры приложения определяются во время сборки, но могут быть переопределены при запуске.
В этом разделе показано, как настроить монолитную и распределенную конфигурации приложений с Docker контейнерами.
7.3.9.1. Развертывание монолитного Uber JAR
Откройте проект CUBA в Студии, выполните действие Deployment settings, перейдите на вкладку Uber JAR и затем отредактируйте настройки, как описано ниже.
Соберите JAR-файл, выполнив команду buildUberJar в Gradle. Вы можете сделать это прямо в Студии, открыв диалог Search, или из командной строки:
gradle buildUberJar
Docker-образ с проектом CUBA должен использовать базовый образ OpenJDK. Для указания информации, необходимой Docker для запуска приложения, используется файл Dockerfile. Dockerfile — это текстовый файл, в котором содержится список команд Docker-клиента. Это простой способ автоматизировать процесс создания образа. В файле можно указать базовый образ Docker для запуска, местоположения кода проекта, а так же необходимые зависимости и команды, которые нужно использовать при запуске контейнера.
### Dockerfile
FROM openjdk:8
COPY . /usr/src/cuba-sales
CMD java -Dapp.home=/usr/src/cuba-sales/home -jar /usr/src/cuba-sales/app.jar
Для получения дополнительной информации об инструкциях Dockerfile см. Dockerfile reference.
Теперь можно создать образ:
docker build -t cuba-sample-sales .
Если у вас нет образа openjdk:8 то Docker-клиент сначала скачает его, а потом создаст образ приложения CUBA.
Для определения и запуска много-контейнерных Docker приложений используется инструмент Docker Compose. Вся конфигурация для docker-compose описывается в файле docker-compose.yml, и с его помощью можно одной командой поднять приложение с необходимым набором сервисов.
Конфигурационный файл будет иметь следующую сруктуру:
version: '2'
services:
postgres:
image: postgres:9.6.6
environment:
- POSTGRES_PASSWORD=cuba
- POSTGRES_USER=cuba
- POSTGRES_DB=sales
ports:
- "5433:5432"
networks:
- sales-network
web:
image: cuba-sample-sales
ports:
- "8080:8080"
networks:
- sales-network
networks:
sales-network:
В этом файле определены два сервиса, web and postgres. Сервис web:
Сервис postgres использует публичный образ Postgres, скачанный из репозитория Docker Hub.
Для запуска приложения перейдите в директорию с файлом docker-compose.yml и выполните команду:
docker-compose up
Приложение будет доступно по адресу http://localhost:8080/app.
7.3.9.2. Развертывание Distributed Uber JAR
Для настройки распределенной конфигурации откройте проект CUBA в Студии, выполните действие Deployment settings, перейдите на вкладку Uber JAR и затем отредактируйте настройки, как описано ниже:
Добавьте следующие настройки в appProperties:
appProperties = ['cuba.automaticDatabaseUpdate': true,
'cuba.webHostName':'sales-core',
'cuba.connectionUrlList': 'http://sales-core:8079/app-core',
'cuba.webAppUrl': 'http://sales-web:8080/app',
'cuba.useLocalServiceInvocation': false,
'cuba.trustedClientPermittedIpList': '*.*.*.*']
|
Если в приложении используется более одного сервера Middleware, их все нужно перечислитьв свойстве cuba.connectionUrlList и настроить кластер серверов Web Client как описано в разделе Масштабирование приложения. |
Пересоберите JAR-файлы с помощью задачи buildUberJar:
gradle buildUberJar
Создайте две подпапки в папке docker-image для web и core JAR-файлов. Так как для распределенного приложения создается отдельный контейнер для каждого JAR-файла, то в этом случае необходимо конфигурировать два файла Dockerfile.
Конфигурационный файл Dockerfile для core имеет вид:
### Dockerfile
FROM openjdk:8
COPY . /usr/src/cuba-sales
CMD java -Dapp.home=/usr/src/cuba-sales/home -jar /usr/src/cuba-sales/app-core.jar
Конфигурационный файл Dockerfile для web имеет вид:
### Dockerfile
FROM openjdk:8
COPY . /usr/src/cuba-sales
CMD java -Dapp.home=/usr/src/cuba-sales/home -jar /usr/src/cuba-sales/app.jar
Файл docker-compose.yml конфигурирует два сервиса core и web и выглядит следующим образом:
version: '2'
services:
postgres:
image: postgres:9.6.6
environment:
- POSTGRES_PASSWORD=cuba
- POSTGRES_USER=cuba
- POSTGRES_DB=sales
ports:
- "5433:5432"
networks:
- sales-network
sales-core:
image: cuba-sample-sales-core
networks:
- sales-network
sales-web:
image: cuba-sample-sales-web
ports:
- "8080:8080"
networks:
- sales-network
networks:
sales-network:
Соберите образы с помощью следующих команд:
docker build -t cuba-sample-sales-web ./web
docker build -t cuba-sample-sales-core ./core
Для запуска приложения перейдите в директорию с файлом docker-compose.yml и запустите следующую команду:
docker-compose up
Приложение будет доступно по адресу http://localhost:8080/app.
|
Для развертывания контейнеров на нескольких физических машинам вам может понадобиться установить и настроить Docker Swarm или Kubernetes. |
7.3.9.3. Плагин Gradle для Docker
В этом разделе на примере монолитной конфигурации Uber JAR показано как с помощью плагина Gradle для Docker создать и опубликовать образ приложения CUBA.
Для сборки образа Docker из Gradle можно использовать плагин: bmuschko/gradle-docker-plugin.
Для использования плагина в файл build.gradle нужно добавить зависимости и импортировать необходимые классы для работы с образами как показано в примере (X.Y.Z нужно заменить на актуальную версию плагина):
buildscript {
dependencies {
classpath 'com.bmuschko:gradle-docker-plugin:X.Y.Z'
}
}
import com.bmuschko.gradle.docker.tasks.image.Dockerfile
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
import com.bmuschko.gradle.docker.tasks.image.DockerPushImage
import com.bmuschko.gradle.docker.DockerRegistryCredentials
Плагин com.bmuschko.docker-remote-api дает возможность взаимодействовать с Docker при помощи удаленного API. Можно смоделировать любой рабочий процесс, создав свою задач на основе пользовательской задачи, предоставляемой плагином. Для того, чтобы воспользоваться плагином, добавьте следующий фрагмент кода в файл build.gradle:
apply plugin: 'com.bmuschko.docker-remote-api'
Dockerfile можно создать с помощью пользовательской задачи Dockerfile. Инструкции для Dockerfile должны соответствовать определенной структуре. Инструкции в файле обрабатываются сверху вниз. Каждая инструкция добавляет новый слой в образ и фиксирует изменения. Docker исполняет инструкции, следуя процессу:
Задача создания Dockerfile будет выглядеть следующим образом:
task createDockerfile(type: Dockerfile, dependsOn: buildUberJar) {
destFile = project.file('build/distributions/uberJar/Dockerfile')
from 'openjdk:8'
addFile("app.jar", "/usr/src/cuba-sales/app.jar")
defaultCommand("java", "-Dapp.home=/usr/src/cuba-sales/home", "-jar", "/usr/src/cuba-sales/app.jar")
}
Операции скачивания и публикации образов в публичный репозиторий Docker Hub или закрытый репозиторий могут потребовать аутентификации. Учетные данные можно передать в метод с помощью объекта registryCredentials. Определите свои учетные данные в файле gradle.properties:
dockerHubEmail = 'example@email.com'
dockerHubPassword = 'docker-hub-password'
dockerHubUsername = 'docker-hub-username'
После этого можно получить доступ к этим свойствам в файле build.gradle по имени:
def dockerRegistryCredentials = new DockerRegistryCredentials()
dockerRegistryCredentials.email = dockerHubEmail
dockerRegistryCredentials.password = dockerHubPassword
dockerRegistryCredentials.username = dockerHubUsername
Для создания образа приложения CUBA с помощью с Dockerfile и публикации этого образа в публичный репозиторий Docker Hub нужно определить следующие задачи:
task buildImage(type: DockerBuildImage, dependsOn: createDockerfile) {
inputDir = createDockerfile.destFile.parentFile
tags = ['sample-sales', '{docker-hub-username}/{default-repo-folder-name}:sample-sales']
registryCredentials = dockerRegistryCredentials
}
task pushImage(type: DockerPushImage, dependsOn: buildImage) {
tag = 'sample-sales'
imageName = '{docker-hub-username}/{default-repo-folder-name}'
registryCredentials = dockerRegistryCredentials
}
Настройте и соберите монолитный Uber JAR как показано в разделе Развертывание монолитного Uber JAR. После этого запустите задачу pushImage из терминала или из поля Search в Студии.
gradle pushImage
Эта задача последовательно собирает Uber JAR, генерирует Dockerfile с необходимыми инструкцими, создает образ и публикует его в репозиторий Docker Hub.
7.3.9.4. Развертывание контейнера в Heroku
Настройте и соберите монолитный Uber JAR как показано в разделе Развертывание монолитного Uber JAR. Создайте аккаунт в Heroku и установите Heroku CLI. Более детально эти действия описаны в разделе Развертывание WAR-файла в Heroku.
Создайте приложение с уникальным именем и подключите к нему базу данных (в примере подключается PostgreSQL с бесплатным тарифным планом hobby-dev) с помощью Heroku CLI:
heroku create cuba-sales-docker --addons heroku-postgresql:hobby-dev
После создания базы данных нужно указать детали для подключения к сервису БД в файле jetty-env.xml.
Откройте проект в IDE, откройте файл jetty-env.xml. Необходимо поменять URL (имя хоста и базы данных), имя пользователя и пароль. Для этого нужно скопировать данные, указанные на сайте, в файл.
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id='wac' class="org.eclipse.jetty.webapp.WebAppContext">
<New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
<Arg/>
<Arg>jdbc/CubaDS</Arg>
<Arg>
<New class="org.apache.commons.dbcp2.BasicDataSource">
<Set name="driverClassName">org.postgresql.Driver</Set>
<Set name="url">jdbc:postgresql://<Host>/<Database></Set>
<Set name="username"><User></Set>
<Set name="password"><Password></Set>
<Set name="maxIdle">2</Set>
<Set name="maxTotal">20</Set>
<Set name="maxWaitMillis">5000</Set>
</New>
</Arg>
</New>
</Configure>
Соберите монолитный Uber JAR-файл с помощью команды Gradle:
gradle buldUberJar
Также необходимо внести изменения в Dockerfile. Прежде всего нужно ограничить объем памяти, потребляемой приложением. Затем нужно получить порт приложения из Heroku и добавить его к образу.
Файл Dockerfile будет выглядеть следующим образом:
### Dockerfile
FROM openjdk:8
COPY . /usr/src/cuba-sales
CMD java -Xmx512m -Dapp.home=/usr/src/cuba-sales/home -jar /usr/src/cuba-sales/app.jar -port $PORT
Откройте командную строку в папке $PROJECT_FOLDER и запустите команды, указанные ниже:
git init
heroku git:remote -a cuba-sales-docker
git add .
git commit -am "Initial commit"
После этого нужно зайти в репозиторий контейнеров, это место для хранения образов в Heroku.
heroku container:login
Теперь можно создать образ и загрузить его в репозиторий контейнеров.
heroku container:push web
Здесь web — тип процесса приложения. При запуске этой команды Heroku по умолчанию создает образ с помощью Dockerfile в текущем каталоге, а затем загружает его в Heroku.
После завершения процесса развертывания ваше приложение будет доступно в браузере по ссылке https://cuba-sales-docker.herokuapp.com/app
Вы также можете открыть приложение с помощью кнопки Open app, расположенной на панели Heroku.
Третий способ открыть запущенное приложение — использовать следующую команду (необходимо добавить app контекст к ссылке, т.е. https://cuba-sales-docker.herokuapp.com/app):
heroku open
7.4. Конфигурация прокси для Tomcat
Для задач интеграции может потребоваться прокси-сервер. В этом разделе описывается конфигурация HTTP-сервера Nginx в качестве прокси для приложения на платформе CUBA.
|
Если вы настраиваете прокси, то не забудьте задать значение в параметре cuba.webAppUrl. |
NGINX
Для Nginx предлагается 2 конфигурации проксирования, описанные ниже. Все примеры подготовлены и проверены на Ubuntu 16.04.
К примеру, ваше веб-приложение работает по ссылке http://localhost:8080/app.
|
Кроме Nginx следует настроить еще и Tomcat. |
Сначала добавьте в конфигурационный файл Tomcat conf/server.xml следующий код:
<Valve className="org.apache.catalina.valves.RemoteIpValve"
remoteIpHeader="X-Forwarded-For"
requestAttributesEnabled="true"
internalProxies="127\.0\.0\.1" />
и перезапустите Tomcat:
sudo service tomcat8 restart
Это требуется для того, чтобы Tomcat мог обрабатывать заголовки от Nginx без модификации веб-приложения.
sudo apt-get install nginx
Откройте в браузере ссылку http://localhost и убедитесь, что стартовая страница Nginx работает.
Теперь вы можете удалить символьную ссылку на тестовый сайт Nginx:
rm /etc/nginx/sites-enabled/default
Далее сконфигурируйте прокси одной из выбранных схем:
В этом случае все запросы обрабатывает прокси, прозрачно перенаправляя их в приложение.
Создайте конфигурационный файл Nginx /etc/nginx/sites-enabled/direct_proxy:
server {
listen 80;
server_name localhost;
location /app/ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Proto $scheme;
# Required to send real client IP to application server
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
# Optional timeouts
proxy_read_timeout 3600;
proxy_connect_timeout 240;
proxy_http_version 1.1;
# Required for WebSocket:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:8080/app/;
}
}
и перезапустите Nginx:
sudo service nginx restart
Вы можете открыть свой сайт по ссылке http://localhost/app.
Проксирование с перенаправлением
В этом примере описано, как изменить путь к приложению в URL с /app на /, как если бы приложение было развёрнуто в корневом контексте (аналог /ROOT). Это позволит вам обращаться к приложению по адресу http://localhost.
Создайте конфигурационный файл Nginx /etc/nginx/sites-enabled/direct_proxy:
server {
listen 80;
server_name localhost;
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Proto $scheme;
# Required to send real client IP to application server
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
# Optional timeouts
proxy_read_timeout 3600;
proxy_connect_timeout 240;
proxy_http_version 1.1;
# Required for WebSocket:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:8080/app/;
# Required for folder redirect
proxy_cookie_path /app /;
proxy_set_header Cookie $http_cookie;
proxy_redirect http://localhost/app/ http://localhost/;
}
}
и перезапустите Nginx
sudo service nginx restart
Ваше приложение доступно по ссылке http://localhost.
|
Обратите внимание, что аналогичные инструкции развертывания прокси справедливы для конфигураций Jetty, WildFly и др. В таких случаях вам может понадобиться дополнительная конфигурация этих серверов. |
7.5. Конфигурация прокси для Uber JAR
В этой части рассказывается, как настроить HTTP-сервер Nginx в качестве прокси для приложения CUBA Uber JAR.
NGINX
Для Nginx предлагается 2 конфигурации проксирования, описанных ниже. Все примеры подготовлены и проверены на Ubuntu 16.04.
К примеру, ваше веб-приложение работает по ссылке http://localhost:8080/app.
|
Приложение Uber JAR использует сервер Jetty версии 9.2. Jetty внутри JAR следует сконфигурировать таким образом, чтобы он обрабатывал заголовки Nginx. |
Сначала создайте конфигурационный файл jetty.xml в корне проекта и вставьте в него следующий код:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Call name="addCustomizer">
<Arg>
<New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/>
</Arg>
</Call>
</New>
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.server.ServerConnector">
<Arg name="server">
<Ref refid="Server"/>
</Arg>
<Arg name="factories">
<Array type="org.eclipse.jetty.server.ConnectionFactory">
<Item>
<New class="org.eclipse.jetty.server.HttpConnectionFactory">
<Arg name="config">
<Ref refid="httpConfig"/>
</Arg>
</New>
</Item>
</Array>
</Arg>
<Set name="port">8080</Set>
</New>
</Arg>
</Call>
</Configure>
Добавьте свойство webJettyConfPath в задачу buildUberJar вашего файла build.gradle:
task buildUberJar(type: CubaUberJarBuilding) {
singleJar = true
coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml'
appProperties = ['cuba.automaticDatabaseUpdate' : true]
webJettyConfPath = 'jetty.xml'
}
Вы можете использовать Studio, чтобы сгенерировать jetty-env.xml, для этого пройдите в Project Properties > Deployment Settings > далее на вкладку Uber Jar. Или используйте пример ниже:
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id='wac' class="org.eclipse.jetty.webapp.WebAppContext">
<New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
<Arg/>
<Arg>jdbc/CubaDS</Arg>
<Arg>
<New class="org.apache.commons.dbcp2.BasicDataSource">
<Set name="driverClassName">org.postgresql.Driver</Set>
<Set name="url">jdbc:postgresql://<Host>/<Database></Set>
<Set name="username"><User></Set>
<Set name="password"><Password></Set>
<Set name="maxIdle">2</Set>
<Set name="maxTotal">20</Set>
<Set name="maxWaitMillis">5000</Set>
</New>
</Arg>
</New>
</Configure>
Соберите Uber JAR, используя следующую команду:
gradlew buildUberJar
Ваше приложение будет расположено в папке build/distributions/uberJar, имя по-умолчанию: app.jar.
Запустите приложение:
java -jar app.jar
Затем установите и настройте Nginx, как описано в секции Tomcat.
В зависимости от выбранной схемы проксирования, ваш сайт будет доступен по одной из ссылок: http://localhost/app или http://localhost.
Используйте тот же самый конфигурационный файл jetty.xml в корне проекта, как описано выше, но не изменяйте build.gradle.
Соберите Uber JAR, используя следующую команду:
gradlew buildUberJar
Ваше приложение будет расположено в папке build/distributions/uberJar, имя по-умолчанию: app.jar.
Запустите приложение с параметром -jettyConfPath:
java -jar app.jar -jettyConfPath jetty.xml
Затем установите и настройте Nginx, как описано в секции Tomcat.
В зависимости от выбранной схемы проксирования и настроек в jetty.xml, ваш сайт будет доступен по одной из ссылок: http://localhost/app или http://localhost.
7.6. Масштабирование приложения
В данном разделе рассмотрены способы масштабирования CUBA-приложения, состоящего из блоков Middleware и Web Client, при возрастании нагрузки и ужесточении требований к отказоустойчивости.
|
Этап 1. Оба блока развернуты на одном сервере приложения. Это простейший вариант, реализуемый стандартной процедурой быстрого развертывания. В данном случае обеспечивается максимальная производительность передачи данных между блоками Web Client и Middleware, так как при включенном свойстве приложения cuba.useLocalServiceInvocation сервисы Middleware вызываются в обход сетевого стека. |
|
|
Этап 2. Блоки Middleware и Web Client развернуты на отдельных серверах приложения. Данный вариант позволяет распределить нагрузку между двумя серверами приложения и более оптимально использовать ресурсы серверов. Кроме того, в этом случае нагрузка от веб-пользователей меньше сказывается на выполнении других процессов. Под другими процессами здесь понимается обслуживание средним слоем других типов клиентов (например Desktop), выполнение задач по расписанию и, возможно, интеграционные задачи. Требования к ресурсам серверов:
В этом и более сложных вариантах развертывания в блоке Web Client свойство приложения cuba.useLocalServiceInvocation должно быть установлено в false, а свойство cuba.connectionUrlList должно содержать URL блока Middleware. |
|
|
Этап 3. Кластер серверов Web Client работает с одним сервером Middleware. Данный вариант применяется, когда вследствие большого количества одновременно подключенных пользователей требования к памяти для блока Web Client превышают возможности одной JVM. В этом случае запускается кластер (два или более) серверов Web Client, и подключение пользователей производится через Load Balancer. Все серверы Web Client работают с одним сервером Middleware. Дублирование серверов Web Client автоматически обеспечивает отказоустойчивость на этом уровне. Однако, так как репликация HTTP-сессий не поддерживается, при незапланированном отключении одного из серверов Web Client все пользователи, подключенные к нему, вынуждены будут выполнить новый логин в приложение. Настройка данного варианта развертывания описана в Настройка кластера Web Client. |
|
|
Этап 4. Кластер серверов Web Client работает с кластером серверов Middleware. Это максимальный вариант развертывания, обеспечивающий отказоустойчивость и балансировку нагрузки для Middleware и Web Client. Подключение пользователей к серверам Web Client производится через Load Balancer. Серверы WebClient работают с кластером серверов Middleware. Для этого им не требуется дополнительный Load Balancer - достаточно определить список URL серверов Middleware в свойстве cuba.connectionUrlList. Можно также использовать дополнение для интеграции с Apache ZooKeeper для динамического обнаружения серверов среднего слоя. В кластере серверов Middleware организуется взаимодействие для обмена информацией о пользовательских сессиях, блокировках и пр. При этом обеспечивается полная отказоустойчивость блока Middleware - при отключении одного из серверов выполнение запросов от клиентских блоков продолжается на доступном сервере прозрачно для пользователей. Настройка данного варианта развертывания описана в Настройка кластера Middleware. |
|
7.6.1. Настройка кластера Web Client
В данном разделе рассматривается следующая конфигурация развертывания:
Здесь на серверах host1 и host2 блок установлены инстансы Tomcat с веб-приложением app, реализующим блок Web Client. Пользователи обращаются к балансировщику нагрузки по адресу http://host0/app, который перенаправляет запрос этим серверам. На сервере host3 установлен Tomcat с веб-приложением app-core, реализующим блок Middleware.
7.6.1.1. Установка и настройка Load Balancer
Рассмотрим процесс установки балансировщика нагрузки на базе Apache HTTP Server для операционной системы Ubuntu 14.04.
$ sudo apt-get install apache2 libapache2-mod-jk
workers.tomcat_home=
workers.java_home=
ps=/
worker.list=tomcat1,tomcat2,loadbalancer,jkstatus
worker.tomcat1.port=8009
worker.tomcat1.host=host1
worker.tomcat1.type=ajp13
worker.tomcat1.connection_pool_timeout=600
worker.tomcat1.lbfactor=1
worker.tomcat2.port=8009
worker.tomcat2.host=host2
worker.tomcat2.type=ajp13
worker.tomcat2.connection_pool_timeout=600
worker.tomcat2.lbfactor=1
worker.loadbalancer.type=lb
worker.loadbalancer.balance_workers=tomcat1,tomcat2
worker.jkstatus.type=status
<VirtualHost *:80>
...
<Location /jkmanager>
JkMount jkstatus
Order deny,allow
Allow from all
</Location>
JkMount /jkmanager/* jkstatus
JkMount /app loadbalancer
JkMount /app/* loadbalancer
</VirtualHost>
$ sudo service apache2 restart
7.6.1.2. Настройка серверов Web Client
|
В примерах ниже пути к конфигурационным файлам приводятся для варианта Быстрое развертывание в Tomcat. |
На серверах Tomcat 1 и Tomcat 2 необходимо произвести следующие настройки:
<Server port="8005" shutdown="SHUTDOWN">
...
<Service name="Catalina">
...
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat1">
...
</Engine>
</Service>
</Server>
cuba.useLocalServiceInvocation = false
cuba.connectionUrlList = http://host3:8080/app-core
cuba.webHostName = host1
cuba.webPort = 8080
cuba.webContextName = app
Параметры cuba.webHostName, cuba.webPort, cuba.webContextName не обязательны для работы кластера WebClient, но позволяют проще идентифицировать сервера в других механизмах платформы, например в консоли JMX. Кроме того, в экране User Sessions в атрибуте Client Info отображается сформированный из этих параметров идентификатор блока Web Client, на котором работает данный пользователь.
7.6.2. Настройка кластера Middleware
В данном разделе рассматривается следующая конфигурация развертывания:
Здесь на серверах host1 и host2 блок установлены инстансы Tomcat с веб-приложением app, реализующим блок Web Client. Настройка кластера этих серверов рассмотрена в предыдущем разделе. На серверах host3 и host4 установлены инстансы Tomcat с веб-приложением app-core, реализующим блок Middleware. Между ними настроено взаимодействие для обмена информацией о пользовательских сессиях и блокировках, сброса кэшей и др.
|
В примерах ниже пути к конфигурационным файлам приводятся для варианта Быстрое развертывание в Tomcat. |
7.6.2.1. Настройка обращения к кластеру Middleware
Для того, чтобы клиентские блоки могли работать с несколькими серверами Middleware, достаточно указать список URL этих серверов в свойстве приложения cuba.connectionUrlList. Для Web Client это можно сделать в файле tomcat/conf/app/local.app.properties:
cuba.useLocalServiceInvocation = false
cuba.connectionUrlList = http://host3:8080/app-core,http://host4:8080/app-core
cuba.webHostName = host1
cuba.webPort = 8080
cuba.webContextName = app
Сервер среднего слоя выбирается в случайном порядке в момент первого обращения для данной пользовательской сессии, и фиксируется на все время жизни сессии ("sticky session"). Запросы от анонимной сессии и без сессии не фиксируются и выполняются на серверах выбираемых в случайном порядке.
Алгоритм выбора сервера предоставляется бином cuba_ServerSorter, который по умолчанию реализован классом RandomServerSorter. В проекте можно реализовать собственный алгоритм выбора.
7.6.2.2. Настройка взаимодействия серверов Middleware
Сервера Middleware могут поддерживать общие списки пользовательских сессий и других объектов, а также координировать сброс кэшей. Для этого достаточно на каждом их них включить свойство приложения cuba.cluster.enabled. Пример файла tomcat/conf/app-core/local.app.properties:
cuba.cluster.enabled = true
cuba.webHostName = host3
cuba.webPort = 8080
cuba.webContextName = app-core
Для серверов Middleware обязательно нужно указать правильные значения свойств cuba.webHostName, cuba.webPort и cuba.webContextName для формирования уникального Server Id.
Механизм взаимодействия основан на библиотеке JGroups. Платформа содержит два конфигурационных файла для JGroups:
Для настройки параметров JGroups для вашего окружения скопируйте подходящий файл jgroups.xml из корня архива cuba-core-<version>.jar в модуль core вашего проекта или в каталог tomcat/conf/app-core, и настройте его нужным образом.
Программный интерфейс для взаимодействия в кластере Middleware обеспечивает бин ClusterManagerAPI. Его можно использовать в приложении - см. JavaDocs и примеры использования в коде платформы.
7.6.2.3. Использование ZooKeeper для координации кластера
Существует компонент приложения, обеспечивающий динамическое обнаружение серверов middleware для коммуникации между блоками middleware и для запросов с клиентских блоков. Он основан на интеграции с Apache ZooKeeper - централизованным сервисом для работы с конфигурационной информацией. Если данный компонент включен в проект, при запуске блоков приложения необходимо указывать только один статический адрес - адрес ZooKeeper. При этом сервера middleware публикуют свои адреса в каталоге ZooKeeper, а механизм обнаружения запрашивает ZooKeeper для получения адресов доступных серверов. Если сервер middleware останавливается, его адрес автоматически исключается из каталога (немедленно или по истечении таймаута).
Исходный код компонента доступен на GitHub, бинарные артефакты опубликованы в репозиториях платформы. См. README для получения информации по установке и использованию компонента.
Server Id служит для надежной идентификации серверов в кластере Middleware. Идентификатор имеет вид host:port/context, например:
tezis.haulmont.com:80/app-core
192.168.44.55:8080/app-core
Идентификатор формируется на основе параметров конфигурации cuba.webHostName, cuba.webPort, cuba.webContextName, поэтому крайне важно корректно указать эти параметры для блока Middleware, работающего в кластере.
Server Id может быть получен c помощью бина ServerInfoAPI или через JMX-интерфейс ServerInfoMBean.
7.7. Использование инструментов JMX
В данном разделе рассмотрены различные аспекты использования инструментов Java Management Extensions в CUBA-приложениях.
Модуль Web Client базового проекта cuba платформы содержит средство просмотра и редактирования JMX объектов. Точкой входа в этот инструмент является экран com/haulmont/cuba/web/app/ui/jmxcontrol/browse/display-mbeans.xml, зарегистрированный под идентификатором jmxConsole и в стандартном меню доступный через пункт Администрирование → Консоль JMX.
Без дополнительной настройки консоль отображает все JMX объекты, зарегистрированные в JVM, на которой работает блок Web Client, к которому в данный момент подключен пользователь. Соответственно, в простейшем случае развертывания всех блоков приложения в одном экземпляре веб-контейнера консоль имеет доступ к JMX бинам всех уровней, а также к JMX объектам самой JVM и веб-контейнера.
Имена бинов приложения имеют префикс, соответствующий имени веб-приложения, их содержащего. Например, бин app-core.cuba:type=CachingFacade загружен веб-приложением app-core, реализующим блок Middleware, а бин app.cuba:type=CachingFacade загружен веб-приложением app, реализующим блок Web Client.
Рисунок 41. JMX консоль
Консоль JMX может также работать с JMX объектами произвольной удаленной JVM. Это актуально в случае развертывания блоков приложения на нескольких экземплярах веб-контейнера, например, отдельно Web Client и Middleware.
Для подключения к удаленной JVM необходимо в поле Соединение JMX консоли выбрать созданное ранее соединение, либо вызвать экран создания нового соединения:
Рисунок 42. Редактирование JMX соединения
Для соединения указывается JMX хост и порт, логин и пароль. Имеется также поле Имя узла, которое заполняется автоматически, если по указанному адресу обнаружен какой-либо блок CUBA-приложения. В этом случае значением этого поля становится комбинация свойств cuba.webHostName и cuba.webPort данного блока, что позволяет идентифицировать содержащий его сервер. Если подключение произведено к постороннему JMX интерфейсу, то поле Имя узла будет иметь значение "Unknown JMX interface". Значение данного поля можно произвольно изменять.
Для подключения удаленной JVM она должна быть соответствующим образом настроена - см. ниже.
7.7.2. Настройка удаленного доступа к JMX
В данном разделе рассматривается настройка запуска сервера Tomcat, необходимая для удаленного подключения к нему инструментов JMX.
7.7.2.1. Tomcat JMX под Windows
set CATALINA_OPTS=%CATALINA_OPTS% ^
-Dcom.sun.management.jmxremote ^
-Djava.rmi.server.hostname=192.168.10.10 ^
-Dcom.sun.management.jmxremote.ssl=false ^
-Dcom.sun.management.jmxremote.port=7777 ^
-Dcom.sun.management.jmxremote.authenticate=true ^
-Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password ^
-Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access
Здесь в параметре java.rmi.server.hostname необходимо указать реальный IP адрес или DNS имя компьютера, на котором запущен сервер, в параметре com.sun.management.jmxremote.port - порт для подключения инструментов JMX.
admin readwrite
admin admin
cacls jmxremote.password /P "domain_name\user_name":R
где domain_name\user_name - домен и имя пользователя.
CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote \
-Djava.rmi.server.hostname=192.168.10.10 \
-Dcom.sun.management.jmxremote.port=7777 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=true"
CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access"
Здесь в параметре java.rmi.server.hostname необходимо указать реальный IP адрес или DNS имя компьютера, на котором запущен сервер, в параметре com.sun.management.jmxremote.port - порт для подключения инструментов JMX.
admin readwrite
admin admin
chmod go-rwx jmxremote.password
Приложения CUBA используют технологию server push в механизме фоновых задач. Это может потребовать дополнительной настройки сервера приложения и прокси-сервера (если таковой используется).
По умолчанию server push использует протокол WebSocket. Следующие свойства приложения влияют на функциональность server push платформы:
Информация ниже взята из статьи с веб-сайта Vaadin - Configuring push for your environment.
Chrome показывает ERR_INCOMPLETE_CHUNKED_ENCODING
Это совершенно нормально и означает, что (long-polling) push-соединение было прервано третьей стороной. Обычно это происходит, когда между браузером и сервером имеется прокси-сервер, а прокси-сервер имеет настроенный тайм-аут и отключает соединение, когда тайм-аут достигнут. После этого браузер должен повторно подключиться к серверу.
java.lang.ClassNotFoundException: org.eclipse.jetty.websocket.WebSocketFactory$Acceptor
Это означает, что где то в classpath развернут Jetty. Atmosphere путается и использует реализацию Websocket от Jetty сервера вместо Tomcat. Одной из распространенных причин этого является то, что Вы случайно развернули vaadin-client-compiler, у которого Jetty есть в качестве зависимости (требуется, например, для SuperDevMode).
Чтобы Streaming режим работал в Glassfish 4, необходимо включить опцию comet.
Для этого установите
(Configurations → server-config → Network Config → Protocols → http-listener-1 → HTTP → Comet Support)
или используйте
asadmin set server-config.network-config.protocols.protocol.http-listener-1.http.comet-support-enabled="true"
Если Вы используете Glassfish 4.0, то для избежания проблем Вам необходимо обновиться на версию Glassfish 4.1.
Используйте WebLogic 12.1.3 или новее. По умолчанию, WebLogic 12 имеет 30 сек. таймаут для websocket соединений. Чтобы избежать постоянных повторных соединений, вы можете установить параметр weblogic.websocket.tyrus.session-max-idle-timeout либо в значение -1 (т.е. без таймаута), либо в большее чем 30000 (значение в миллисекундах).
JBoss EAP 6.4 включает поддержку websockets, но по умолчанию они отключены. Чтобы включить websockets, Вам необходимо переключить JBoss в режим использования NIO-коннектора, запустив:
$ bin/jboss-cli.sh --connect
затем выполнив следующие команды:
batch
/subsystem=web/connector=http/:write-attribute(name=protocol,value=org.apache.coyote.http11.Http11NioProtocol)
run-batch
:reload
Чтобы включить websockets, необходимо добавить WEB-INF/jboss-web.xml в Ваш war-файл со следующим содержанием:
<jboss-web version="7.2" xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee schema/jboss-web_7_2.xsd">
<enable-websockets>true</enable-websockets>
</jboss-web>
Если логи сервера содержат
Duplicate resource xyz-abc-def-ghi-jkl. Could be caused by a dead connection not detected by your server. Replacing the old one with the fresh one
Это указывает на то, что сначала браузер подключился к серверу и использовал данный идентификатор для push-соединения. Позже браузер (возможно, тот же самый) снова подключился с использованием того же идентификатора, но сервер считает, что старое соединение с браузером все еще активно. Сервер закрывает старое соединение и регистрирует предупреждение.
Это происходит из-за того, что между браузером и сервером существует прокси-сервер, а прокси-сервер настроен так, чтобы убивать открытые соединения после определенного таймаута бездействия (данные не отправляются до того, как сервер выдает команду push). Из-за того, как работает TCP/IP, сервер не знает, что соединение было убито и продолжает думать, что старый клиент подключен, и все в порядке.
У вас есть несколько вариантов, чтобы избежать этой проблемы:
Если пользователи подключаются к серверу приложения через прокси, не поддерживающий WebSocket, рекомендуется установить свойство cuba.web.pushLongPolling в true и увеличить таймаут запроса на прокси до 10 минут или больше.
Ниже приведен пример конфигурации веб-сервера Nginx для использования WebSocket:
location / {
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600;
proxy_connect_timeout 240;
proxy_set_header Host $host;
proxy_set_header X-RealIP $remote_addr;
proxy_pass http://127.0.0.1:8080/;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Каждый блок приложения, развернутый как веб-приложение, предоставляет URL для проверки своего состояния. HTTP GET запрос на этот URL возвращает ok если блок готов к работе.
Пути URL для различных блоков перечислены ниже:
То есть для приложения с именем app, развернутого на localhost:8080, адреса будут следующими:
Ответ ok можно заменить на произвольный текст с помощью свойства приложения cuba.healthCheckResponse.
Контроллеры проверки посылают события типа HealthCheckEvent. Следовательно, вы можете добавить собственную логику проверки работоспособности приложения. В примере на GitHub, бин web-уровня реагирует на события проверки и вызывает сервис среднего слоя, который в свою очередь выполняет операцию на базе данных.
В данном разделе приведены практические рекомендации по работе с базой данных на этапах разработки и эксплуатации приложения.
Информация о том, как сконфигурировать приложение для работы с некоторой СУБД, приведена в разделе Компоненты работы с базой данных.
В процессе разработки приложения возникает необходимость создания и поддержания схемы базы данных, соответствующей моделируемым сущностям. Для решения этой задачи платформа предлагает подход на основе скриптов создания и обновления БД. Далее рассматриваются практические шаги по применению этого подхода.
Задача по созданию и поддержке схемы БД состоит из двух частей: создание скриптов и их выполнение.
Скрипты могут быть созданы как вручную, так и с помощью Studio. Рассмотрим процесс создания скриптов в Studio. Для этого выполните команду Generate DB scripts, расположенную в секции Entities. При этом Studio подключается к базе данных, определенной на странице Project properties, и сравнивает имеющуюся схему БД с текущей моделью данных.
Если база данных отсутствует, либо в ней нет таблиц SYS_DB_CHANGELOG и SEC_USER, то генерируются только скрипты инициализации БД. В противном случае создаются также и скрипты обновления. Затем открывается страница, содержащая сгенерированные скрипты.
На вкладке Update scripts отображаются скрипты обновления. Скрипты со статусом new отражают разницу между текущим состоянием модели данных и схемы БД. Для каждой создаваемой или изменяемой таблицы создается отдельный скрипт. В отдельные скрипты помещаются также наборы ограничений целостности таблиц (referential integrity constraints). При закрытии страницы нажатием OK скрипты сохраняются в каталог db/update/{db_type} модуля core.
Со статусом applied отображаются скрипты, уже имеющиеся в проекте и примененные в БД ранее. Они не могут быть отредактированы или удалены.
На вкладке Update scripts могут также отображаться скрипты со статусом to be deleted. Это файлы, имеющиеся в проекте, но не примененные в БД. При закрытии страницы нажатием OK эти файлы будут удалены. Это нормально, если эти скрипты были созданы вами при предыдущей генерации, но не были применены вызовом Update database. В этом случае они больше не нужны, так как текущая разница между схемой БД и моделью данных отражена в новых только что сгенерированных скриптах. Если же, например, эти скрипты получены вами из системы контроля версий от другого разработчика, то вам следует отменить сохранение и сначала применить чужие скрипты на своей БД, а уже потом генерировать новые.
Вкладки Init tables, Init constraints, Init data отображают скрипты создания БД, располагающиеся в каталоге db/init/{db_type} модуля core.
На вкладке Init tables отображается скрипт создания таблиц 10.create-db.sql. Код, относящийся к одной таблице, отделяется комментариями begin {table_name} ... end {table_name}. При изменении некоторой сущности в модели Studio заменит код только между комментариями для соответствующей таблицы, не трогая остальной код, в который могли быть внесены ручные изменения. Поэтому при ручном редактировании не удаляйте эти комментарии, иначе Studio не сможет правильно встраивать свои изменения в существующие файлы.
На вкладке Init constraints отображается скрипт создания ограничений целостности 20.create-db.sql. В нем также присутствуют разделяющие таблицы комментарии, которые нельзя удалять.
На вкладке Init data отображается скрипт 30.create-db.sql, предназначенный для внесения дополнительной информации при инициализации БД. Это могут быть, например, функции, триггеры, или DML операторы для наполнения базы необходимыми данными. Содержимое данного скрипта создается вручную при необходимости.
|
На начальной стадии разработки приложения, когда модель данных активно меняется, рекомендуется пользоваться только скриптами создания БД (расположенными на вкладках Init tables, Init constraints, Init data), а скрипты обновления на вкладке Update scripts удалять сразу после вызова команды Generate DB scripts. Это наиболее простой и надежный способ поддержания БД в актуальном состоянии. Разумеется, он имеет один существенный недостаток - при применении скриптов БД пересоздается с нуля, поэтому все внесенные в нее данные теряются. Этот недостаток можно частично компенсировать на этапе разработки, добавив в скрипт Init data команды для создания первичных данных при инициализации. Скрипты обновления становятся удобным и необходимым инструментом разработки и сопровождения БД на более позднем этапе, когда модель данных относительно стабильна, а в базах данных у разработчиков и в эксплуатации накоплены данные, которые нельзя терять при пересоздании БД с нуля. |
Для применения скриптов используйте механизм выполнения скриптов БД задачами Gradle: чтобы пересоздать базу данных полностью, вызовите в главном меню пункт Run → Create database, а чтобы применить скрипты обновления - пункт Run → Update database. Следует иметь в виду, что эти пункты доступны, только если сервер приложения остановлен. Разумеется, соответствующие задачи Gradle (createDb и updateDb) можно вызвать в любой момент из командной строки, но если при этом база данных или какие-либо ее объекты заняты, выполнение скриптов завершится с ошибкой.
8.2. Особенности MS SQL Server
Microsoft SQL Server использует кластерные индексы для таблиц.
По умолчанию кластерный индекс создается по первичному ключу таблицы, однако используемые в CUBA-приложении ключи типа UUID плохо подходят для кластерного индекса. Мы рекомендуем создавать первичные ключи типа UUID с модификатором nonclustered:
create table SALES_CUSTOMER (
ID uniqueidentifier not null,
CREATE_TS datetime,
...
primary key nonclustered (ID)
)^
8.3. Особенности Oracle Database
В связи с политикой распространения JDBC драйвера Oracle его можно скачать только вручную с сайта http://www.oracle.com/technetwork/database/features/jdbc/index-091264.html.
После скачивания скопируйте JAR с драйвером ojdbc6.jar в подкаталог lib Studio и подкаталог lib установленного сервера Tomcat. После этого необходимо остановить Studio, остановить демона Gradle, выполнив
gradle --stop
в командной строке, а затем снова запустить Studio.
Для версии Studio SE скопируйте JAR-файл с драйвером в подкаталог ~/.haulmont/studio/lib/ Studio и подкаталог lib установленного сервера Tomcat.
JDBC-драйвер MySQL не распространяется в составе CUBA Studio в связи с его лицензией, поэтому его нужно установить отдельно:
|
Если в вашей базе данных используется кодировка, отличная от UTF-8, некоторые скрипты фреймворка не смогут быть выполнены. В этом случае необходимо изменить свойства базы данных Charset и Collation name, передав следующие параметры подключения в поле Connection params в окне редактирования свойств проекта: ?useUnicode=true&characterEncoding=UTF-8 |
MySQL не поддерживает частичные (partial) индексы, поэтому единственная возможность создать ограничение уникальности для soft deleted сущности - это использовать в составе индекса колонку DELETE_TS. Однако, существует другая проблема: MySQL позволяет иметь несколько NULLs в колонке с ограничением уникальности. Так как стандартная колонка DELETE_TS является nullable, она не может быть использована в уникальном индексе. Рекомендуется следующий способ создания уникальных ограничений для сущностей с мягким удалением:
create table DEMO_CUSTOMER (
...
DELETE_TS_NN datetime(3) not null default '1000-01-01 00:00:00.000',
...
)
create trigger DEMO_CUSTOMER_DELETE_TS_NN_TRIGGER before update on DEMO_CUSTOMER
for each row
if not(NEW.DELETE_TS <=> OLD.DELETE_TS) then
set NEW.DELETE_TS_NN = if (NEW.DELETE_TS is null, '1000-01-01 00:00:00.000', NEW.DELETE_TS);
end if
create unique index IDX_DEMO_CUSTOMER_UNIQ_NAME on DEMO_CUSTOMER (NAME, DELETE_TS_NN)
8.5. Использование произвольной схемы БД
PostgreSQL и Microsoft SQL Server поддерживают подключение к произвольной схеме внутри базы данных. По умолчанию на PostgreSQL используется схема public, на SQL Server - схема dbo.
PostgreSQL
Для использования произвольной схемы на PostgreSQL укажите параметр currentSchema в свойстве connectionParams задач сборки createDb и updateDb, например:
task createDb(dependsOn: assembleDbScripts, type: CubaDbCreation) {
dbms = 'postgres'
host = 'localhost'
dbName = 'my_db'
connectionParams = '?currentSchema=my_schema'
dbUser = 'cuba'
dbPassword = 'cuba'
}
При использовании Studio, добавьте этот параметр в поле Connection params страницы Project properties. При этом Studio автоматически обновит build.gradle. После этого можно запускать обновление или пересоздание БД, все таблицы будут созданы в указанной схеме.
Microsoft SQL Server
На Microsoft SQL Server указания параметра подключения недостаточно, необходимо также создать связь между пользователем БД и схемой. Ниже приведен пример создания базы данных и использования схемы.
create login JohnDoe with password='saPass1'
create database my_db
create schema my_schema
create user JohnDoe for login JohnDoe with default_schema = my_schema
exec sp_addrolemember 'db_owner', 'JohnDoe'
После этого необходимо указать параметр подключения currentSchema в свойстве connectionParams задачи сборки updateDb (или в свойствах проекта в Studio). На самом деле, данный параметр никак не обрабатывается драйвером JDBC для SQL Server, но он указывает Studio и плагину Gradle какую схему использовать.
task updateDb(dependsOn: assembleDbScripts, type: CubaDbUpdate) {
dbms = 'mssql'
dbmsVersion = '2012'
host = 'localhost'
dbName = 'my_db'
connectionParams = ';currentSchema=my_schema'
dbUser = 'JohnDoe'
dbPassword = 'saPass1'
}
Имейте в виду, что пересоздавать БД SQL Server из Studio или выполнением createDb в командной строке нельзя, так как использование не-дефолтной схемы требует ассоциации с пользователем. Однако можно выполнять Update database в Studio или updateDb в командной строке, и все необходимые таблицы будут созданы в существующей базе данных и в указанной схеме.
8.6. Создание и обновление БД при эксплуатации приложения
В данном разделе рассматриваются способы создания и обновления базы данных на этапе развертывания и эксплуатации приложения. Для знакомства с устройством и правилами создания скриптов БД см. Скрипты создания и обновления БД и Создание схемы БД.
8.6.1. Использование механизма выполнения скриптов БД сервером
Механизм выполнения скриптов БД сервером можно использовать как для первичной инициализации базы данных, так и для ее последующего обновления в процессе развития приложения и изменения схемы данных.
Чтобы инициализировать новую базу данных, нужно выполнить следующее:
cuba.automaticDatabaseUpdate = true
В случае быстрого развертывания в Tomcat этот файл находится в каталоге tomcat/conf/app-core. Если файл не существует, создайте его.
В дальнейшем при каждом старте сервера приложения механизм выполнения скриптов будет сравнивать набор скриптов, находящийся в каталоге скриптов базы данных, со списком выполненных скриптов, зарегистрированным в БД. При появлении в каталоге новых скриптов они будут выполнены и также зарегистрированы. Таким образом, достаточно в каждую новую версию приложения включать скрипты обновления, и при рестарте сервера приложения база данных будет приводиться в актуальное состояние.
При эксплуатации механизма выполнения скриптов на старте сервера следует иметь в виду следующее:
Для выяснения причин сбоя необходимо открыть файл лога app.log в каталоге журналов сервера и найти сообщения о выполнении SQL от логгера com.haulmont.cuba.core.sys.DbUpdaterEngine, и, возможно, последующие сообщения об ошибках.
В связи с этим рекомендуется непосредственно перед запуском сервера с новой версией приложения делать резервное сохранение БД. Тогда после устранения причины ошибки достаточно восстановить БД и запустить автоматический процесс вновь.
Если бэкап БД отсутствует, то после устранения причины ошибки необходимо выяснить, какая часть вызвавшего ошибку скрипта выполнилась и закоммичена. Если скрипт не выполнился целиком, то можно сразу снова запускать автоматический процесс. Если же часть команд до ошибочной была отделена символом "^", выполнялась в отдельной транзакции и была закоммичена, то необходимо выполнить оставшуюся часть команд, а затем зарегистрировать данный скрипт в SYS_DB_CHANGELOG вручную. После этого можно стартовать сервер, механизм автоматического обновления продолжит работу со следующего невыполненного скрипта.
CUBA Studio генерирует скрипты обновления с символом ";" в качестве разделителями для всех типов БД, кроме Oracle. Если команды скрипта разделены точками с запятой, они выполняются в одной транзакции, и в случае ошибки скрипт откатывается целиком. Тем самым обеспечивается постоянное соответствие между структурой БД и списком выполненных скриптов обновления.
8.6.2. Инициализация и обновление БД из командной строки
Скрипты создания и обновления БД могут быть запущены из командной строки с помощью класса com.haulmont.cuba.core.sys.utils.DbUpdaterUtil, входящего в состав блока Middleware платформы. При запуске должны быть переданы следующие аргументы:
Пример скрипта для Linux, запускающего DbUpdaterUtil:
#!/bin/sh
DB_URL="jdbc:postgresql://localhost/mydb"
APP_CORE_DIR="./../webapps/app-core"
WEBLIB="$APP_CORE_DIR/WEB-INF/lib"
SCRIPTS="$APP_CORE_DIR/WEB-INF/db"
TOMCAT="./../lib"
SHARED="./../shared/lib"
CLASSPATH=""
for jar in `ls "$TOMCAT/"`
do
CLASSPATH="$TOMCAT/$jar:$CLASSPATH"
done
for jar in `ls "$WEBLIB/"`
do
CLASSPATH="$WEBLIB/$jar:$CLASSPATH"
done
for jar in `ls "$SHARED/"`
do
CLASSPATH="$SHARED/$jar:$CLASSPATH"
done
java -cp $CLASSPATH com.haulmont.cuba.core.sys.utils.DbUpdaterUtil \
-dbType postgres -dbUrl $DB_URL \
-dbUser $1 -dbPassword $2 \
-scriptsDir $SCRIPTS \
-$3
Данный скрипт рассчитан на работу с БД с именем mydb, расположенной на локальном сервере PostgreSQL. Скрипт должен быть расположен в каталоге bin сервера Tomcat, и запускаться с параметрами {имя пользователя}, {пароль}, {команда}, например:
./dbupdate.sh cuba cuba123 update
Ход выполнения скриптов отображается в консоли. При возникновении ошибок обновления следует поступать так же, как описано в предыдущем разделе для механизма автоматического обновления.
|
При обновлении БД из командной строки имеющиеся Groovy-скрипты запускаются, но реально отрабатывает только их основная часть. По причине отсутствия контекста сервера PostUpdate-часть игнорируется с выдачей в консоль соответствующего сообщения. |
8.7. Подключение к HSQLDB с помощью Squirrel SQL
HSQLDB, он же HyperSQL, является удобной СУБД для прототипирования приложений, так как не требует установки, и запускается автоматически в CUBA Studio, если для проекта выбрано использование этой СУБД. В данном разделе описан способ подключения к базе данных HSQLDB через внешний инструмент, позволяющий работать со структурой и данными напрямую средствами SQL.
SQuirreL SQL Client является свободно распространяемым Java-приложением, позволяющим работать с базами данных через JDBC. Загрузить Squirrel SQL можно по адресу http://squirrel-sql.sourceforge.net.
Перед запуском Squirrel SQL найдите файл hsqldb-x.x.x.jar в подкаталоге lib каталога установки CUBA Studio и скопируйте его в подкаталог lib каталога установки Squirrel SQL.
Запустите Squirrel SQL и откройте вкладку Drivers. Убедитесь что драйвер HSQLDB Server активен.
Откройте вкладку Aliases и нажмите на кнопку Create a new Alias.
В открывшемся окне укажите параметры подключения - Database URL, пользователя и пароль. По умолчанию пользователь - sa, пароль отсутствует. Database URL можно найти на вкладке Project properties в CUBA Studio или скопировать из файла modules/core/web/META-INF/context.xml проекта.
Платформа CUBA включает в себя следующие средства разграничения прав доступа пользователей к информации:
9.1. Безопасность веб-приложения
Защищены ли CUBA приложения?
Фреймворк CUBA Platform следует лучшим практикам безопасности и предоставляет автоматическую защиту от большинства самых распространённых уязвимостей веб-приложений. Архитектура платформы реализует безопасную модель программирования, позволяя вам сконцентрироваться на бизнес задачах и логике приложений.
Web Client - серверное приложение, в котором всё состояние вашего приложения, бизнес- и UI-логика расположены на сервере. В отличие от фронтенд-фреймворков, исполняющих код UI в веб-браузере, веб-клиент никогда не открывает серверные структуры данных для клиентской части, где уязвимости могут быть использованы злонамеренным кодом. Валидация данных всегда выполняется на сервере, гарантируя, что злонамеренные пользователи не смогут её обойти, то же самое применимо и к валидации данных в универсальном REST API.
Веб-клиент имеет встроенную защиту от атак межсайтового скриптинга (XSS). Все выводимые данные автоматически преобразуются в HTML формат перед тем как они будут показаны в веб-браузере.
Все запросы между сервером и клиентом включают CSRF токен, специфичный для пользовательской сессии. Фреймворк Vaadin обеспечивает всё взаимодействие между сервером и клиентом, поэтому вам не потребуется добавлять CSRF токен к каждому запросу вручную.
Все сетевые вызовы в веб-клиенте проходят через один веб-сервис, используемый для вызова удалённых процедур (RPC) из клиентской части UI компонентов. Приложения не открывают доступ к своей бизнес-логике в виде веб-сервисов, поэтому пользователям доступно меньшее количество точек входа для возможных атак.
Универсальный REST API автоматически применяет разрешения ролей и все ограничения (constraints) как для пользователей системы, выполнивших вход, так и при анонимном доступе.
Платформа использует слой ORM на базе EclipseLink, реализующую защиту от SQL инъекций. Параметры SQL запросов всегда передаются в JDBC в виде массива параметров и не подставляются в SQL запросы в виде строк.
9.2. Компоненты подсистемы безопасности
Основные компоненты подсистемы безопасности CUBA приведены на следующей диаграмме.
Рисунок 43. Диаграмма компонентов подсистемы безопасности
Рассмотрим их более подробно.
Security management screens - имеющиеся в платформе экраны, с помощью которых администратором системы осуществляется настройка прав доступа пользователей.
Login screen − окно входа в систему. В этом окне производится аутентификация пользователя по имени учетной записи и паролю. В базе данных вместо пароля, в целях его безопасности, хранится хэш.
После входа пользователя в систему создается объект UserSession − пользовательская сессия. Это центральный элемент обеспечения безопасности, объект, ассоциированный с аутентифицированным в данный момент в системе пользователем и содержащий информацию о правах доступа пользователя к данным.
Процесс входа пользователя в систему подробно описан в разделе Вход в систему.
Roles − роли пользователей. Роль - это объект системы, которому с одной стороны сопоставляется набор разрешений, необходимых для выполнения конкретных функций, а с другой стороны − подмножество пользователей, которые должны иметь эти разрешения.
Разрешения бывают следующих типов:
Access Groups - группы доступа пользователей. Группы представляют собой иерархическую структуру, каждый элемент которой задает набор ограничений (Constraints), позволяющих контролировать доступ на уровне отдельных экземпляров (строк таблицы) некоторой сущности. Например, пользователь видит только те документы, которые были созданы в его отделе.
Окно входа в систему (Login screen) предназначено для регистрации пользователя путем ввода логина и пароля. Логин не чувствителен к регистру вводимых символов.
Управлять отображением флажка Remember Me в веб-клиенте можно с помощью свойства приложения cuba.web.rememberMeEnabled. Стандартное окно входа содержит также выпадающий список поддерживаемых системой языков. Отображение списка и его содержимое определяются комбинацией свойств приложения cuba.localeSelectVisible и cuba.availableLocales.
См. также свойства приложения cuba.web.loginDialogDefaultUser, cuba.web.loginDialogDefaultPassword и cuba.web.loginDialogPoweredByLinkVisible.
В веб-клиенте стандартное окно логина можно кастомизировать или полностью заменить в проекте, используя ссылку Generic UI > New > Login Window в Studio. См. также Специфика Web Client.
В платформе имеется механизм защиты от взлома пароля методом перебора, см. свойство приложения cuba.bruteForceProtection.enabled.
Для более глубокой кастомизации процесса аутентификации, см. разделы Вход в систему и Процесс входа в Web Client.
Для каждого пользователя системы создается соответствующий экземпляр сущности sec$User. Он содержит уникальный логин, хэш пароля, ссылку на группу доступа, список ролей и другие атрибуты. Управление пользователями осуществляется с помощью экрана Administration → Users:
Помимо стандартных действий создания, изменения и удаления записей имеются следующие:
|
Для того, чтобы пользователь мог сменить себе пароль, у него должно быть право на экран sec$User.changePassword. Имейте это в виду при конфигурировании ролей, особенно если вы назначаете всем пользователям Denying роль. Данный экран можно найти внутри элемента Other screens в редакторе роли. |
Шаблоны этих email можно отредактировать. Для этого необходимо создать в модуле core шаблоны по аналогии с reset-password-subject.gsp и reset-password-body.gsp. Для локализации можно добавить соответствующие файлы шаблонов с суффиксами локалей, как это сделано в платформе.
В шаблонах используется синтаксис Groovy SimpleTemplateEngine, что позволяет использовать блоки кода Groovy прямо в тексте шаблона. Например:
Hello <% print (user.active ? user.login : 'user') %>
<% if (user.email.endsWith('@company.com')) { %>
The password for your account has been reset.
Please login with the following temporary password:
${password}
and immediately create a new one for the further work.
Thank you
<% } else {%>
Please contact your system administrator for a new password.
<%}%>
В шаблонах доступны следующие переменные: user, password и persistence. Вы также можете использовать бины Spring среднего слоя, для этого импортируйте класс AppBeans и получите нужный бин с помощью метода AppBeans.get().
После переопределения шаблонов в app.properties модуля core необходимо задать следующие свойства:
cuba.security.resetPasswordTemplateBody = <relative path to your file>
cuba.security.resetPasswordTemplateSubject = <relative path to your file>
Для удобства настройки в production, шаблоны можно разместить или переопределить в конфигурационном каталоге и задать свойства в local.app.properties.
Рассмотрим экран редактирования пользователя:
Маска представляет собой список адресов через запятую. Поддерживаются как адреса формата IPv4, так и адреса формата IPv6. В первом случае адрес должен состоять из четырех чисел, разделенных точками, при этом любая часть вместо числа может содержать знак "*", что означает "любое число". Адрес в формате IPv6 представляет собой восемь групп по четыре шестнадцатеричные цифры, разделенных двоеточием. Любая группа также может быть заменена знаком "*".
Маска может содержать адреса только одного формата. Наличие адресов формата IPv4 и IPv6 одновременно недопустимо.
Пример: 192.168.*.*
9.2.2.1. Замещение пользователей
Администратор системы может дать возможность пользователю замещать другого пользователя. При этом у замещающего пользователя сессия не меняется, а подменяется набор ролей, ограничений и атрибутов. Все эти параметры текущий пользователь получает от замещаемого пользователя.
|
В прикладном коде для получения текущего пользователя рекомендуется использовать метод UserSession.getCurrentOrSubstitutedUser() возвращающий либо замещаемого пользователя, либо пользователя, выполнившего логин (если замещения в данный момент нет). В то же время механизмы аудита платформы (атрибуты createdBy и updatedBy, журнал изменений и снимки сущностей) всегда регистрируют пользователя, который произвел логин, а не замещаемого пользователя. |
Если пользователь имеет замещаемых пользователей, то в правом верхнем углу главного окна приложения вместо простой надписи с именем текущего пользователя отображается выпадающий список:
При выборе другого пользователя в этом списке все открытые экраны будут закрыты, и произойдет замещение. После этого метод UserSession.getUser() по-прежнему будет возвращать пользователя, выполнившего логин в систему, а метод UserSession.getSubstitutedUser() - замещенного пользователя. Если замещения нет, метод UserSession.getSubstitutedUser() возвращает null.
Управление замещаемыми пользователями производится с помощью таблицы Substituted Users экрана редактирования пользователя. Рассмотрим экран добавления замещаемого пользователя:
Все значения даты и времени по умолчанию отображаются в соответствии с часовым поясом сервера. Часовой пояс сервера возвращается методом TimeZone.getDefault() блока приложения. По умолчанию, платформа получает часовой пояс из операционной системы, однако его можно явно задать системным свойством Java user.timezone. Например, чтобы задать часовой пояс по Гринвичу для веб-клиента и Middleware, работающих на сервере Tomcat под Unix, нужно добавить в файл tomcat/bin/setenv.sh следующее свойство:
CATALINA_OPTS="$CATALINA_OPTS -Duser.timezone=GMT"
Пользователь может просматривать и редактировать значения типа timestamp в часовых поясах, отличных от часового пояса сервера. Существует два способа управления часовыми поясами пользователя:
В обоих случаях, часовой пояс настраивается при помощи двух полей:
Если оба поля пусты, часовые пояса для пользователя не конвертируются. В противном случае, платформа сохраняет часовой пояс в объекте UserSession при логине и использует его для ввода и отображения значений типа timestamp. Значение, возвращаемое методом UserSession.getTimeZone() может также использоваться и в прикладном коде.
Если часовой пояс используется для текущей сессии, его краткое имя и отклонение от времени по Гринвичу отображаются в главном окне приложения рядом с именем текущего пользователя.
|
Преобразование часовых поясов выполняется только для атрибутов типа DateTimeDatatype, то есть, содержащих timestamp. Атрибуты, хранящие только дату (DateDatatype) или время (TimeDatatype) по отдельности, не конвертируются. Вы можете запретить преобразование отдельных timestamp-атрибутов, установив для них аннотацию @IgnoreUserTimeZone. |
Разрешение определяет право пользователя на какой-либо объект или функциональность системы: экран, операцию над сущностью и так далее. Разрешение в зависимости от своего значения может как дать пользователю право на объект, так и отобрать его (то есть по сути являться запрещением).
|
Если явного разрешения на объект не установлено, пользователь имеет право на этот объект. |
Разрешения представляются экземплярами сущности sec$Permission и содержат следующие атрибуты:
Рассмотрим типы разрешений:
В атрибуте target указывается идентификатор экрана, атрибут value может иметь значения 0 или 1 (экран запрещен или разрешен соответственно).
Права на экраны проверяются при построении главного меню системы и при каждом вызове методов openWindow(), openEditor(), openLookup() интерфейса Frame.
Для проверки права на экран в прикладном коде используйте метод isScreenPermitted() интерфейса Security.
В атрибуте target указывается имя сущности и через символ ":" имя операции: create, read, update, delete. Например: library$Book:delete. Атрибут value может иметь значения 0 или 1 (операция запрещена или разрешена соответственно).
Права на операции с сущностью проверяются при работе с данными через DataManager, а также в связанных с данными визуальных компонентах и стандартных действиях со списками сущностей. В результате права на операции оказывают влияние на поведение клиентских блоков и REST API. При работе с данными непосредственно на Middleware через EntityManager права не проверяются.
Для проверки права на операцию c сущностью в прикладном коде используйте метод isEntityOpPermitted() интерфейса Security.
В атрибуте target указывается имя сущности и через символ ":" имя атрибута, например: library$Book:name. Атрибут value может иметь значения 0, 1 или 2 (атрибут скрыт, только для чтения, или полностью разрешен соответственно).
Права на атрибуты сущностей проверяются только в связанных с данными визуальных компонентах и REST API.
Для проверки права на атрибут сущности в прикладном коде используйте метод isEntityAttrPermitted() интерфейса Security.
В атрибуте target указывается код функциональности, атрибут value может иметь значения 0 или 1 (запрещено или разрешено соответственно).
Набор специфических разрешений для данного проекта задается в конфигурационном файле permissions.xml.
Пример использования:
@Inject
private Security security;
private void calculateBalance() {
if (!security.isSpecificPermitted("myapp.calculateBalance"))
return;
...
}
В атрибуте target указывается идентификатор экрана и через символ ":" путь к компоненту. Описание формата пути см. в следующем разделе.
|
Для проверки разрешений вместо непосредственного использования методов класса UserSession рекомендуется использовать аналогичные методы интерфейса Security, принимающие во внимание возможное расширение сущностей. |
Роль объединяет набор разрешений, которые могут быть предоставлены пользователю.
Пользователь может иметь несколько ролей. При этом он получает логическую сумму (ИЛИ) прав на некоторый объект от всех ролей, которые у него есть. Например, если пользователю назначены роли A, B и C, роль A запрещает X, роль B разрешает X, роль C не устанавливает явных разрешений на X, то в итоге X будет разрешен.
Если ни одна роль пользователя не определяет явно разрешения на объект, то пользователь имеет право на данный объект. Таким образом, пользователь имеет права на все объекты, на которые либо ни одна роль явно не определяет разрешения, либо хотя бы одна роль определяет, что право есть.
|
Если пользователю дать единственную роль без явно установленных разрешений, или не давать никаких ролей вообще, то у него будут все права на все объекты. |
Список ролей отображается экраном Administration → Roles. Здесь помимо стандартных действий создания, изменения и удаления записей имеется кнопка Assign to users, позволяющая назначить выбранную роль сразу нескольким пользователям.
Рассмотрим экран редактирования роли. В верхней его части отображаются атрибуты роли:
Роли всех типов могут иметь явно установленные разрешения, например в Read-only роль можно добавить разрешения на модификацию некоторых сущностей. Однако для роли Super явная установка каких-либо запрещений не имеет смысла, так как наличие роли данного типа в любом случае отменяет все запрещения.
Ниже представлены вкладки управления разрешениями:
Дерево в левой части вкладки отражает структуру главного меню системы. Последним элементом дерева является Other screens, внутри которого сосредоточены экраны, не включенные в главное меню (например, экраны редактирования сущностей).
При переходе на данную вкладку изначально включен флажок Assigned only, поэтому в таблице отображаются только сущности, для которых в данной роли уже есть явные разрешения. Поэтому для новой роли таблица пуста. Для установки разрешений снимите флажок Assigned only и нажмите Apply. Список сущностей можно фильтровать, вводя в поле Entity любую часть имени сущности и нажимая Apply.
Установив флажок System level, можно выбрать системную сущность, помеченную аннотацией @SystemLevel. По умолчанию такие сущности не показываются в таблице.
При нарушении ограничений на операции с сущностями пользователю будет показано сообщение об ошибке. Для локализации таких сообщений нужно переопределить ключи для обработчика RowLevelSecurityExceptionHandler в главном пакете сообщений.
В таблице сущностей в колонке Permissions отображается список атрибутов, для которых явно указаны разрешения. Зеленым цветом обозначено разрешение modify (полный доступ), синим цветом - read-only (только чтение), красным - hide (атрибут скрыт).
Управление списком сущностей аналогично описанному для вкладки Entities.
|
Если требуется динамически изменять доступ к атрибуту в зависимости от текущего состояния экземпляра сущности или связанных сущностей, используйте также механизм контроля доступа к атрибутам сущностей с помощью интерфейса SetupAttributeAccessHandler. |
Имена объектов, на которые могут быть назначены специфические разрешения, определяются в конфигурационном файле permissions.xml проекта.
Разрешения данного типа дают возможность ограничить доступ к любому компоненту экрана, в том числе не связанному с данными (например, к контейнеру). Для создания таких разрешений необходимо знать идентификаторы компонентов, а значит, иметь доступ к исходному коду экранов.
Для создания ограничения выберите нужный экран в выпадающем списке Screen, задайте путь к компоненту в поле Component, и нажмите Add. После этого установите режим доступа к выбранному компоненту в панели Permissions.
Правила формирования пути к компоненту:
Группы доступа позволяют организовывать пользователей в иерархическую структуру для установки ограничений и для присвоения произвольных атрибутов сессии.
Пользователь может быть причислен только к одной группе, однако он получит список ограничений и атрибутов сессии от всех групп вверх по иерархии.
Управление группами доступа осуществляется в экране Administration → Access Groups:
Ограничения (Constraints) можно наложить на различные действия с экземплярами сущностей. В отличие от разрешений, которые накладываются на класс сущностей, ограничения накладываются на конкретные экземпляры, не соответствующие заданным условиям. Ограничения можно накладывать на чтение, создание, модификацию и удаление сущностей. Кроме того, можно задать специальные ограничения, не привязанные к этим четырем действиям.
|
Пользователь получает список ограничений от всех групп начиная со своей и вверх по иерархии. Тем самым реализуется принцип: чем ниже пользователь в иерархии групп, тем больше у него ограничений. |
Следует отметить, что все ограничения проверяются при операциях с данными, осуществляемых клиентом через стандартный DataManager. В случае несоответствия проверяемой сущности условиям ограничений при создании, модификации или удалении, выбрасывается исключение RowLevelSecurityException.
Существует несколько типов проверки ограничений: проверка в базе данных, проверка в памяти, проверка в базе данных и памяти.
Для создания ограничения в экране Access Groups выберите группу, на которую нужно наложить ограничение, перейдите на вкладку Constraints и нажмите Create:
Далее выберите сущность в выпадающем списке Entity Name, тип операции в выпадающем списке Operation Type, и тип проверки в выпадающем списке Check Type. В зависимости от выбранного тип проверки вам нужно будет задать JPQL условия в полях Join Clause и Where Clause и/или Groovy условие в поле Groovy Script. Кроме того вы можете воспользоваться мастером созданий ограничений доступа (Constraint Wizard). Мастер позволяет визуально задавать Groovy и JPQL условия. Если вы выбрали тип операции "Специальные операции", появляется обязательное поле Код, где нужно указать строку, по которой будет идентифицироваться данное ограничение.
|
Редактор JPQL в полях Join Clause и Where Clause поддерживает автодополнение имен сущностей и их атрибутов. Для вызова автодополнения нажмите Ctrl+Space. Если вызов произведен после точки, будет выведен список атрибутов сущности, соответствующей контексту, иначе - список всех сущностей модели данных. |
Правила формирования ограничения:
Простейший пример ограничения приведен на рисунке выше: пользователи с данным ограничением будут видеть только те экземпляры сущности ref$Car, у которых поле VIN начинается с '00'.
Ещё один классический пример: некая сущность связана с сущностью User в отношении many-to-many, и необходимо, чтобы пользователю были доступны только те экземпляры сущности, в которых есть ссылка непосредственно на него. В этом случае можно использовать оператор member of в поле Where Clause:
(select u from sec$User u where u.id = :session$userId) member of {E}.users
Для ограничений с проверкой в памяти в Groovy скрипт передается переменная userSession типа UserSession. Ее можно использовать для получения атрибутов текущей пользовательской сессии, например:
{E}.createdBy == userSession.user.login
Разработчик может проверить условия ограничений для конкретной сущности с помощью методов интерфейса Security:
Кроме того, существует возможность связать любое действие, унаследованное от класса ItemTrackingAction, c проверкой ограничений. Для этого в XML-элементе action следует задать атрибут constraintOperationType, либо использовать метод setConstraintOperationType() в контроллере экрана.
Пример:
<table>
...
<actions>
<action id="create"/>
<action id="edit" constraintOperationType="update"/>
<action id="remove" constraintOperationType="delete"/>
</actions>
</table>
|
При нарушении ограничения пользователю показывается уведомление. Заголовок и текст уведомления для каждого ограничения можно переопределить прямо в приложении. Для этого необходимо выбрать ограничение на вкладке Constraints экрана Access Groups, нажать на кнопку Localization и в появившемся окне задать свой заголовок и текст сообщения. |
Группа доступа может определять список атрибутов сессии для пользователей, входящих в данную группу. Эти атрибуты можно использовать при настройке ограничений. Кроме того, на этапе разработки в прикладной код системы можно заложить анализ наличия некоторых атрибутов сессии, и тем самым управлять поведением готовой системы для конкретных групп пользователей на этапе эксплуатации.
В пользовательскую сессию при входе в систему будут помещены все атрибуты, заданные для группы, в которой находится пользователь, и для всех родительских групп вверх по иерархии. При этом если атрибут встречается в иерархии групп несколько раз, значение он получит от самой верхней группы, то есть переопределение значений атрибутов на нижнем уровне невозможно. При попытке переопределения в журнал сервера будет выведено сообщение с уровнем WARN.
Для создания атрибута в экране Access Groups выберите группу, перейдите на вкладку Session Attributes и нажмите Create:
В данном экране необходимо задать уникальное имя атрибута, тип данных и значение.
Получить атрибут сессии в коде приложения можно следующим способом:
@Inject
private UserSessionSource userSessionSource;
...
Integer accessLevel = userSessionSource.getUserSession().getAttribute("accessLevel");
Использовать атрибут в ограничениях можно, указав его в параметре JPQL с префиксом session$:
{E}.accessLevel = :session$accessLevel
9.3. Примеры управления доступом
В данном разделе приведены практические рекомендации по настройке доступа пользователей к данным.
Рекомендованный способ настройки ролей и разрешений:
Разумеется, ничто не мешает совмещать обе стратегии.
9.3.2. Создание локальных администраторов
Иерархическая структура групп доступа с наследованием ограничений позволяет создавать локальных администраторов и делегировать им создание пользователей и настройку их прав в рамках подразделений организации.
Локальному администратору доступны экраны подсистемы безопасности, однако он видит только пользователей и группы в своей группе доступа и ниже. Он может создавать подгруппы и пользователей и назначать им имеющиеся в системе роли. При этом все создаваемые им пользователи будут иметь как минимум те же ограничения, что и он сам.
Глобальный администратор, находящийся в корневой группе доступа, лишенной ограничений, должен создать роли, которые будут доступны локальным администраторам для назначения пользователям. Сами локальные администраторы не должны иметь прав на создание и изменение ролей.
Рассмотрим следующую структуру групп доступа:
Задача:
Способ решения задачи:
{E}.id in (
select h.group.id from sec$GroupHierarchy h
where h.group.id = :session$userGroupId or h.parent.id = :session$userGroupId
)
Это ограничение не позволяет пользователям видеть группы выше своей собственной.
{E}.group.id in (
select h.group.id from sec$GroupHierarchy h
where h.group.id = :session$userGroupId or h.parent.id = :session$userGroupId
)
Это ограничение не позволяет пользователям видеть других пользователей, входящих в группы выше своей собственной.
({E}.description is null or {E}.description not like '[hide]')
Данное ограничение не позволяет пользователям видеть роли, в атрибуте description которых записана строка [hide].
Все создаваемые пользователи, включая локальных администраторов, будут получать роль local_user. Эта роль невидима для пользователей группы Departments, поэтому даже локальные администраторы не смогут ее с себя снять. В результате они смогут оперировать только существующими ролями, созданными для них глобальным администратором. Разумеется, эти роли не должны отменять запрещений, введенных ролью local_user.
Интеграция CUBA-приложения c LDAP позволяет решить две задачи:
В режиме интеграции с LDAP пользователи по-прежнему должны иметь учетную запись в приложении. Все разрешения и параметры пользователя (кроме пароля) хранятся в БД приложения, LDAP используется только для аутентификации, т.е. проверки имени и пароля. Пароль в приложении для большинства пользователей, за исключением тех, кому требуется стандартная аутентификация (см. ниже), рекомендуется не задавать вообще. Поле пароля в экране редактирования пользователя не является обязательным к заполнению, если свойство cuba.web.requirePasswordForNewUsers установлено в false.
Если логин пользователя перечислен в свойстве приложения cuba.web.standardAuthenticationUsers, то он всегда аутентифицируется обычным способом через хранимый в базе данных приложения хэш пароля. Поэтому если для некоторого пользователя из данного списка пароль в приложении задан, он сможет войти в систему с этим паролем, если в LDAP такого пользователя нет.
Взаимодействие CUBA-приложения с LDAP осуществляется через бин LdapLoginProvider.
Для расширенной интеграции с Active Directory и обеспечения Single Sign-On для пользователей домена Windows можно использовать библиотеку Jespa и соответствующую имплементацию CubaAuthProvider, которая описана в Интеграция с Active Directory с использованием Jespa.
Вы можете реализовать свой механизм входа при помощи интерфейсов LoginProvider, HttpRequestFilter и обработчиков событий, как это описано в разделе Специфика процесса входа в Web Client.
Также вы можете включить LDAP аутентификацию для клиентов REST API: LDAP аутентификация для REST API.
9.4.1. Базовая интеграция с LDAP
Класс LdapLoginProvider используется по умолчанию при включенном свойстве приложения cuba.web.ldap.enabled. В этом случае для аутентификации пользователей используется библиотека Spring LDAP.
Для настройки интеграции используются следующие свойства приложения блока Web Client:
Пример содержимого файла local.app.properties блока Web Client:
cuba.web.ldap.enabled = true
cuba.web.ldap.urls = ldap://192.168.1.1:389
cuba.web.ldap.base = ou=Employees,dc=mycompany,dc=com
cuba.web.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com
cuba.web.ldap.password = system_user_password
См. также cuba.web.requirePasswordForNewUsers.
В случае интеграции с Active Directory, при создании пользователей в приложении указывайте в качестве логина их sAMAccountName без имени домена.
9.4.2. Интеграция с Active Directory с использованием Jespa
Jespa − библиотека для Java, обеспечивающая расширенную интеграцию между службой каталогов Active Directory и Java-приложениями по протоколу NTLMv2. Подробно о библиотеке см. http://www.ioplex.com.
9.4.2.1. Подключение библиотеки
Загрузите библиотеку с сайта http://www.ioplex.com и разместите JAR в каком-либо репозитории, зарегистрированном в вашем скрипте сборки build.gradle. Это может быть mavenLocal() или репозиторий вашей организации.
В файле build.gradle в секции конфигурации модуля web добавьте зависимости:
configure(webModule) {
...
dependencies {
compile('com.company.thirdparty:jespa:1.1.17') // from a custom repository
compile('jcifs:jcifs:1.3.17') // from Maven Central
...
Создайте в модуле web класс реализации интерфейса CubaAuthProvider:
package com.company.sample.web;
import com.haulmont.cuba.core.global.AppBeans;
import com.haulmont.cuba.core.global.Configuration;
import com.haulmont.cuba.core.global.GlobalConfig;
import com.haulmont.cuba.core.global.Messages;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.security.global.LoginException;
import com.haulmont.cuba.web.auth.CubaAuthProvider;
import com.haulmont.cuba.web.auth.DomainAliasesResolver;
import jespa.http.HttpSecurityService;
import jespa.ntlm.NtlmSecurityProvider;
import jespa.security.PasswordCredential;
import jespa.security.SecurityProviderException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class JespaAuthProvider extends HttpSecurityService implements CubaAuthProvider {
private static class DomainInfo {
private String bindStr;
private String acctName;
private String acctPassword;
private DomainInfo(String bindStr, String acctName, String acctPassword) {
this.acctName = acctName;
this.acctPassword = acctPassword;
this.bindStr = bindStr;
}
}
private static Map<String, DomainInfo> domains = new HashMap<>();
private static String defaultDomain;
private Log log = LogFactory.getLog(getClass());
@Inject
private Configuration configuration;
@Inject
private Messages messages;
@SuppressWarnings("deprecation")
@Override
public void init(FilterConfig filterConfig) throws ServletException {
initDomains();
Map<String, String> properties = new HashMap<>();
properties.put("jespa.bindstr", getBindStr());
properties.put("jespa.service.acctname", getAcctName());
properties.put("jespa.service.password", getAcctPassword());
properties.put("jespa.account.canonicalForm", "3");
properties.put("jespa.log.path", configuration.getConfig(GlobalConfig.class).getLogDir() + "/jespa.log");
properties.put("http.parameter.anonymous.name", "anon");
fillFromSystemProperties(properties);
try {
super.init(properties);
} catch (SecurityProviderException e) {
throw new ServletException(e);
}
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
if (httpServletRequest.getHeader("User-Agent") != null) {
String ua = httpServletRequest.getHeader("User-Agent").toLowerCase();
boolean windows = ua.contains("windows");
boolean gecko = ua.contains("gecko") && !ua.contains("webkit");
if (!windows && gecko) {
chain.doFilter(request, response);
return;
}
}
super.doFilter(request, response, chain);
}
@Override
public void authenticate(String login, String password, Locale loc) throws LoginException {
DomainAliasesResolver aliasesResolver = AppBeans.get(DomainAliasesResolver.NAME);
String domain;
String userName;
int atSignPos = login.indexOf("@");
if (atSignPos >= 0) {
String domainAlias = login.substring(atSignPos + 1);
domain = aliasesResolver.getDomainName(domainAlias).toUpperCase();
} else {
int slashPos = login.indexOf('\\');
if (slashPos <= 0) {
throw new LoginException("Invalid name: %s", login);
}
String domainAlias = login.substring(0, slashPos);
domain = aliasesResolver.getDomainName(domainAlias).toUpperCase();
}
userName = login;
DomainInfo domainInfo = domains.get(domain);
if (domainInfo == null) {
throw new LoginException("Unknown domain: %s", domain);
}
Map<String, String> params = new HashMap<>();
params.put("bindstr", domainInfo.bindStr);
params.put("service.acctname", domainInfo.acctName);
params.put("service.password", domainInfo.acctPassword);
params.put("account.canonicalForm", "3");
fillFromSystemProperties(params);
NtlmSecurityProvider provider = new NtlmSecurityProvider(params);
try {
PasswordCredential credential = new PasswordCredential(userName, password.toCharArray());
provider.authenticate(credential);
} catch (SecurityProviderException e) {
throw new LoginException("Authentication error: %s", e.getMessage());
}
}
private void initDomains() {
String domainsStr = AppContext.getProperty("cuba.web.activeDirectoryDomains");
if (!StringUtils.isBlank(domainsStr)) {
String[] strings = domainsStr.split(";");
for (int i = 0; i < strings.length; i++) {
String domain = strings[i];
domain = domain.trim();
if (!StringUtils.isBlank(domain)) {
String[] parts = domain.split("\\|");
if (parts.length != 4) {
log.error("Invalid ActiveDirectory domain definition: " + domain);
break;
} else {
domains.put(parts[0], new DomainInfo(parts[1], parts[2], parts[3]));
if (i == 0)
defaultDomain = parts[0];
}
}
}
}
}
public String getDefaultDomain() {
return defaultDomain != null ? defaultDomain : "";
}
public String getBindStr() {
return getBindStr(getDefaultDomain());
}
public String getBindStr(String domain) {
initDomains();
DomainInfo domainInfo = domains.get(domain);
return domainInfo != null ? domainInfo.bindStr : "";
}
public String getAcctName() {
return getAcctName(getDefaultDomain());
}
public String getAcctName(String domain) {
initDomains();
DomainInfo domainInfo = domains.get(domain);
return domainInfo != null ? domainInfo.acctName : "";
}
public String getAcctPassword() {
return getAcctPassword(getDefaultDomain());
}
public String getAcctPassword(String domain) {
initDomains();
DomainInfo domainInfo = domains.get(domain);
return domainInfo != null ? domainInfo.acctPassword : "";
}
public void fillFromSystemProperties(Map<String, String> params) {
for (String name : AppContext.getPropertyNames()) {
if (name.startsWith("jespa.")) {
params.put(name, AppContext.getProperty(name));
}
}
}
}
9.4.2.2. Настройка конфигурации
Например:
cuba.web.activeDirectoryDomains = MYCOMPANY|mycompany.com|JESPA$@MYCOMPANY.COM|password1;TEST|test.com|JESPA$@TEST.COM|password2
cuba.web.externalAuthentication = true
cuba.web.externalAuthenticationProviderClass = com.company.sample.web.JespaAuthProvider
jespa.log.level=3
Если приложение развернуто на Tomcat, лог-файл Jespa находится в tomcat/logs.
9.5. Single-Sign-On для приложений CUBA
Single-Sign-On (единый вход, SSO) для приложений CUBA позволяет пользователям входить в несколько запущенных приложений, введя единые имя и пароль один раз в течение сессии веб-браузера.
При использовании SSO существуют два типа приложений:
Приложение может одновременно выполнять функции IDP и SP, то есть установка отдельного IDP не требуется. Функциональность SSO предоставляется модулем cuba-idp, входящим в состав блока Web Client. Приложение можно разрабатывать как обычно, а SSO настроить уже на этапе деплоймента, если требуется.
|
CUBA SSO использует собственный протокол, основанный на HTTP, и на данный момент не поддерживает интеграции с системами, использующими стандартные протоколы аутентификации, такие как SAML или OIDC. |
При использовании SSO, когда пользователь заходит на адрес SP, он перенаправляется на страницу IDP для ввода имени и пароля. После успешной аутентификации, IDP перенаправляет пользователя обратно в приложение SP, где пользователь входит автоматически.
Для настройки SSO необходимо выполнить следующее:
<servlet>
<servlet-name>idp</servlet-name>
<servlet-class>com.haulmont.idp.sys.CubaIdpServlet</servlet-class>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>idp</servlet-name>
<url-pattern>/idp/*</url-pattern>
</servlet-mapping>
<filter>
<filter-name>idpSpringSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>contextAttribute</param-name>
<param-value>org.springframework.web.servlet.FrameworkServlet.CONTEXT.idp</param-value>
</init-param>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>springSecurityFilterChain</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>idpSpringSecurityFilterChain</filter-name>
<url-pattern>/idp/*</url-pattern>
</filter-mapping>
cuba.idp.serviceProviderUrls = http://fish:8081/app/,http://chips:8082/app/
cuba.idp.serviceProviderUrlMasks = http://your-fish.com/.*,http://your-chips.com/.*
cuba.idp.serviceProviderLogoutUrls = http://fish:8081/app/dispatch/idpc/logout,http://chips:8082/app/dispatch/idpc/logout
cuba.webAppUrl = http://fish:8081/app/
cuba.web.idp.baseUrl = http://main:8080/app/idp/
Форма логина IDP
Файлы формы логина располагаются в каталоге idp веб-приложения. В случае деплоймента в Tomcat это каталог tomcat/webapps/app/idp. Стандартные файлы можно заменить, создав файлы с такими же именами в каталоге web/idp модуля web проекта.
По умолчанию форма логина IDP использует механизм локализации на основе JavaScript-библиотеки webL10n, и содержит сообщения для английской и русской локали. Чтобы создать сообщения на других языках, создайте файл modules/web/web/idp/l10n/locales.ini и задайте в нем список файлов сообщений:
[*]
@import url(messages.properties)
[ru]
@import url(messages_ru.properties)
[es]
@import url(messages_es.properties)
Дополнительные файлы сообщений должны располагаться в этом же каталоге(modules/web/web/idp/l10n). В качестве шаблона для файлов сообщений используйте файлы из модуля cuba-idp, который доступен в виде JAR-зависимости в вашем проекте.
Можно также полностью заменить форму логина путем создания собственных файлов login.html и js/login.js, либо изменить стили в файле css/login.css.
Реализация IDP
Точки входа в IDP находятся в контроллерах Spring MVC cuba_IdpController и cuba_IdpServiceController. Для реализации собственного поведения можно создать свои контроллеры и зарегистрировать их под этими же именами в файле idp-dispatcher-spring.xml модуля web.
Стандартная реализация хранит сессии IDP на среднем слое и реплицирует их в кластере. Эта функциональность обеспечивается бином cuba_IdpSessionStore. Механизм хранения сессий может быть кастомизирован путем создания бина с таким же именем в модуле core проекта и регистрации его в соответствующем файле spring.xml. Подробнее см. Расширение бизнес-логики.
В данном разделе рассмотрен пример настройки SSO для двух приложений: Fish и Chips. Fish будет одновременно выполнять роль Identity Provider и Service Provider, Chips будет являться Service Provider.
127.0.0.1 fish
127.0.0.1 chips
|
Проект |
HTTP port |
AJP port |
Shutdown port |
|
Fish |
8081 |
8011 |
8051 |
|
Chips |
8082 |
8012 |
8052 |
<servlet>
<servlet-name>idp</servlet-name>
<servlet-class>com.haulmont.idp.sys.CubaIdpServlet</servlet-class>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>idp</servlet-name>
<url-pattern>/idp/*</url-pattern>
</servlet-mapping>
<filter>
<filter-name>idpSpringSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>contextAttribute</param-name>
<param-value>org.springframework.web.servlet.FrameworkServlet.CONTEXT.idp</param-value>
</init-param>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>springSecurityFilterChain</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>idpSpringSecurityFilterChain</filter-name>
<url-pattern>/idp/*</url-pattern>
</filter-mapping>
cuba.idp.serviceProviderUrls = http://fish:8081/app/,http://chips:8082/app/
cuba.idp.serviceProviderLogoutUrls = http://fish:8081/app/dispatch/idpc/logout,http://chips:8082/app/dispatch/idpc/logout
cuba.idp.trustedServicePassword = mdgh12SSX_pic2
cuba.webAppUrl = http://fish:8081/app/
cuba.web.idp.enabled = true
cuba.web.idp.baseUrl = http://fish:8081/app/idp/
cuba.web.idp.trustedServicePassword = mdgh12SSX_pic2
cuba.webAppUrl = http://chips:8082/app/
cuba.web.idp.enabled = true
cuba.web.idp.baseUrl = http://fish:8081/app/idp/
cuba.web.idp.trustedServicePassword = mdgh12SSX_pic2
Вход через социальные сети, или social login, это разновидность single sign-on, которая позволяет использовать данные для входа в социальные сети, такие как Facebook, Twitter или Google+, для входа в приложения CUBA вместо того, чтобы создавать пользователя в приложении напрямую.
В этом примере мы рассмотрим, как можно войти в приложение, используя аккаунт на Facebook. В Facebook используется механизм авторизации OAuth2, более подробно о его использовании вы можете узнать из документации по Facebook API и Facebook Login Flow: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow.
Исходный код проекта из этого примера доступен на GitHub, ниже приведены ключевые моменты реализации social login.
facebook.appId = 123456789101112
facebook.appSecret = 123456789101112abcde131415fghi16
Также необходимо зарегистрировать URL, который вы указали при регистрации приложения на Facebook, в свойстве приложения cuba.webAppUrl в модулях core и web, к примеру:
cuba.webAppUrl = http://cuba-fb.test:8080/app
@Column(name = "FACEBOOK_ID")
protected String facebookId;
SocialRegistrationService.java
public interface SocialRegistrationService {
String NAME = "demo_SocialRegistrationService";
User findOrRegisterUser(String facebookId, String email, String name);
}
SocialRegistrationServiceBean.java
@Service(SocialRegistrationService.NAME)
public class SocialRegistrationServiceBean implements SocialRegistrationService {
@Inject
private Metadata metadata;
@Inject
private Persistence persistence;
@Inject
private Configuration configuration;
@Override
@Transactional
public User findOrRegisterUser(String facebookId, String email, String name) {
EntityManager em = persistence.getEntityManager();
TypedQuery<SocialUser> query = em.createQuery("select u from sec$User u where u.facebookId = :facebookId",
SocialUser.class);
query.setParameter("facebookId", facebookId);
query.setViewName(View.LOCAL);
SocialUser existingUser = query.getFirstResult();
if (existingUser != null) {
return existingUser;
}
SocialRegistrationConfig config = configuration.getConfig(SocialRegistrationConfig.class);
Group defaultGroup = em.find(Group.class, config.getDefaultGroupId(), View.MINIMAL);
SocialUser user = metadata.create(SocialUser.class);
user.setFacebookId(facebookId);
user.setEmail(email);
user.setName(name);
user.setGroup(defaultGroup);
user.setActive(true);
user.setLogin(email);
em.persist(user);
return user;
}
}
facebook.fields = id,name,email
facebook.scope = email
В этом методе мы добавим к текущей сессии обработчик запроса, затем сохраним текущий URL и перенаправим пользователя на экран авторизации Facebook в браузере:
private RequestHandler facebookCallBackRequestHandler =
this::handleFacebookCallBackRequest;
private URI redirectUri;
@Inject
private FacebookService facebookService;
@Inject
private GlobalConfig globalConfig;
public void loginFacebook() {
VaadinSession.getCurrent()
.addRequestHandler(facebookCallBackRequestHandler);
this.redirectUri = Page.getCurrent().getLocation();
String loginUrl = facebookService.getLoginUrl(globalConfig.getWebAppUrl(), OAuth2ResponseType.CODE);
Page.getCurrent()
.setLocation(loginUrl);
}
В методе handleFacebookCallBackRequest() будет обработан обратный вызов после формы авторизации Facebook. Во-первых, используем экземпляр UIAccessor, чтобы зафиксировать состояние UI, пока будет обрабатываться запрос на вход.
Затем с помощью FacebookService получим email и id учётной записи Facebook. После этого найдём соответствующего пользователя CUBA по его facebookId или создадим нового на лету.
Далее будет совершен вход в приложение, будет создана новая сессия от лица этого пользователя и обновлен UI. Теперь мы можем удалить обработчик обратного вызова Facebook, так как процедура аутентификации закончена.
public boolean handleFacebookCallBackRequest(VaadinSession session, VaadinRequest request,
VaadinResponse response) throws IOException {
if (request.getParameter("code") != null) {
uiAccessor.accessSynchronously(() -> {
try {
String code = request.getParameter("code");
FacebookUserData userData = facebookService.getUserData(globalConfig.getWebAppUrl(), code);
User user = socialRegistrationService.findOrRegisterUser(
userData.getId(), userData.getEmail(), userData.getName());
App app = App.getInstance();
Connection connection = app.getConnection();
Locale defaultLocale = messages.getTools().getDefaultLocale();
connection.login(new ExternalUserCredentials(user.getLogin(), defaultLocale));
} catch (Exception e) {
log.error("Unable to login using Facebook", e);
} finally {
session.removeRequestHandler(facebookCallBackRequestHandler);
}
});
((VaadinServletResponse) response).getHttpServletResponse().
sendRedirect(ControllerUtils.getLocationWithoutParams(redirectUri));
return true;
}
return false;
}
Теперь при нажатии кнопки Facebook на экране входа приложение запросит разрешение на использование учётных данных пользователя Facebook, и если разрешение будет получено, пользователь после логина будет перенаправлен на главную страницу приложения.
Вы можете реализовать свой механизм входа при помощи интерфейсов LoginProvider, HttpRequestFilter и обработчиков событий, как это описано в разделе Процесс входа в Web Client.
Приложение A: Конфигурационные файлы
В данном приложении описаны основные конфигурационные файлы, входящие в состав CUBA-приложений.
Файл app-component.xml требуется для того, чтобы данное приложение можно было использовать в качестве компонента другого приложения. Файл определяет зависимости от других компонентов, описывает существующие модули приложения, генерируемые артефакты и предоставляемые свойства приложения.
Файл app-component.xml должен располагаться в пакете, указанном в элементе App-Component-Id манифеста JAR модуля global. Данный элемент манифеста позволяет системе сборки находить компоненты проекта, находящиеся в class path в момент сборки. В результате, для подключения некоторого компонента к проекту, достаточно добавить координаты артефакта модуля global компонента в элементе dependencies/appComponent файла build.gradle проекта.
По соглашению, файл app-component.xml располагается в корневом пакете проекта (заданном в metadata.xml), который также равен группе артефактов проекта (заданной в build.gradle):
App-Component-Id == root-package == cuba.artifact.group == e.g. 'com.company.sample'
Для генерации файла app-component.xml и элементов манифеста рекомендуется использовать CUBA Studio.
Подключение зависимостей как appJars:
Если компонент содержит сторонние библиотеки, которые вы хотите использовать как артефакты модулей другого приложения (например, app-comp-core или app-comp-web), так, чтобы они были развёрнуты в каталоге tomcat/webapps/app[-core]/WEB-INF/lib/, эти зависимости необходимо добавить как библиотеки appJar:
<module blocks="core"
dependsOn="global,jm"
name="core">
<artifact appJar="true"
name="cuba-jm-core"/>
<artifact classifier="db"
configuration="dbscripts"
ext="zip"
name="cuba-jm-core"/>
<!-- Specify only the artifact name for your appJar 3rd party library -->
<artifact name="javamelody-core"
appJar="true"
library="true"/>
</module>
|
В случае, если вы не планируете использовать проект в качестве компонента других приложений, сторонние зависимости нужно указывать как appJars в задаче deploy файла build.gradle: configure(coreModule) { //... task deploy(dependsOn: assemble, type: CubaDeployment) { appName = 'app-core' appJars('app-global', 'app-core', 'javamelody-core') } //... } |
Файл context.xml является дескриптором развертывания приложения на сервере Apache Tomcat. В развернутом приложении этот файл располагается в подкаталоге META-INF каталога веб-приложения или WAR-файла, например, tomcat/webapps/app-core/META-INF/context.xml. В проекте файлы данного типа находятся в каталогах /web/META-INF модулей core, web, portal.
Основное предназначение файла для блока Middleware - определить JDBC источник данных и поместить его в JNDI под именем, заданным свойством приложения cuba.dataSourceJndiName.
Пример определения источника данных для PostgreSQL:
<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="org.postgresql.Driver"
username="cuba"
password="cuba"
url="jdbc:postgresql://localhost/sales"/>
Пример определения источника данных для Microsoft SQL Server 2005:
<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="net.sourceforge.jtds.jdbc.Driver"
username="sa"
password="saPass1"
url="jdbc:jtds:sqlserver://localhost/sales"/>
Пример определения источника данных для Microsoft SQL Server 2008+:
<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="net.sourceforge.jtds.jdbc.Driver"
username="sa"
password="saPass1"
url="jdbc:jtds:sqlserver://localhost/sales"/>
Пример определения источника данных для Oracle:
<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="oracle.jdbc.OracleDriver"
username="sales"
password="sales"
url="jdbc:oracle:thin:@//localhost:1521/orcl"/>
Пример определения источника данных для MySQL:
<Resource
type="javax.sql.DataSource"
name="jdbc/CubaDS"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="com.mysql.jdbc.Driver"
password="cuba"
username="cuba"
url="jdbc:mysql://localhost/sales?useSSL=false&allowMultiQueries=true"/>
Следующая строка отключает сериализацию HTTP-сессий:
<Manager pathname=""/>
A.3. default-permission-values.xml
Файлы данного типа описывают разрешения пользователя по умолчанию. Разрешения по умолчанию используются тогда, когда ни одна из имеющихся ролей не задаёт разрешения на конкретный экран или функциональность. Разрешения необходимы по большей части для запрещающих ролей: без этого файла пользователь с запрещающей ролью не будет иметь доступа к главному экрану и экранам фильтров.
Данный файт Studio не создаёт автоматически, его нужно создать вручную в модуле core.
Расположение файла задается в свойстве приложения [cuba.defaultPermissionValuesConfig]. Если свойство не задано, будет использован файл платформы по умолчанию - cuba-default-permission-values.xml.
Схема XML доступна по адресу http://schemas.haulmont.com/cuba/default-permission-values.xsd.
Рассмотрим структуру файла.
default-permission-values - корневой элемент. Он содержит всего один вложенный элемент - permission.
permission - само разрешение: определяет, на какой тип объектов накладывается разрешение или запрет.
У permission есть три атрибута:
Пример:
<?xml version="1.0" encoding="UTF-8"?>
<default-permission-values xmlns="http://schemas.haulmont.com/cuba/default-permission-values.xsd">
<permission target="dynamicAttributesConditionEditor" value="0" type="10"/>
<permission target="dynamicAttributesConditionFrame" value="0" type="10"/>
<permission target="sec$Filter:read" value="1" type="20"/>
<permission target="cuba.gui.loginToClient" value="1" type="40"/>
</default-permission-values>
Файлы данного типа определяют конфигурацию дополнительного контейнера Spring Framework для клиентских блоков, содержащих контроллеры Spring MVC.
Дополнительный контейнер контроллеров создается таким образом, что основной контейнер (конфигурируемый файлами spring.xml) является родительским по отношению к нему. Это означает, что бины контейнера контроллеров могут обращаться к бинам основного контейнера, а бины основного контейнера "не видят" контейнер контроллеров.
Расположения файла dispatcher-spring.xml задается в свойстве приложения cuba.dispatcherSpringContextConfig.
Модули web и portal платформы уже содержат такие файлы конфигурации: соответственно cuba-dispatcher-spring.xml и cuba-portal-dispatcher-spring.xml.
Если вы создали контроллеры Spring MVC в своем проекте (например, в модуле web), добавьте следующую конфигурацию:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<context:annotation-config/>
<context:component-scan base-package="com.company.sample.web.controller"/>
</beans>
cuba.dispatcherSpringContextConfig = +com/company/sample/web/dispatcher-config.xml
Контроллеры, созданные в модуле web, будут доступны по адресам, начинающимся с URL сервлета dispatcher (по умолчанию /dispatch). Например:
http://localhost:8080/app/dispatch/my-controller-endpoint
Контроллеры, созданные в модуле portal, будут доступны в корневом контексте веб-приложения, например:
http://localhost:8080/app-portal/my-controller-endpoint
Файлы данного типа используются в блоках Web Client и Desktop Client, реализующих универсальный пользовательский интерфейс, для описания структуры главного меню приложения.
Схема XML доступна по адресу http://schemas.haulmont.com/cuba/6.10/menu.xsd.
Расположение файла menu.xml задается в свойстве приложения cuba.menuConfig. При создании нового проекта в Studio, она создает файл web-menu.xml в корневом пакете модуля web, например modules/web/src/com/company/sample/web-menu.xml.
Рассмотрим структуру файла.
menu-config - корневой элемент
Элементы menu-config, образующие древовидную структуру:
Атрибуты menu:
Элементы menu:
Атрибуты item:
<item bean="cuba_Messages"
beanMethod="getMainMessagePack"/>
<item class="com.haulmont.cuba.core.sys.SecurityContextAwareRunnable"/>
<item id="ref$Colour.browse"
description="mainMsg://carsColours"/>
<item screen="sec$User.browse"/>
shortcut="ALT-C"
shortcut="ALT-CTRL-C"
shortcut="ALT-CTRL-SHIFT-C"
Горячие клавиши можно также задавать в свойствах приложения и использовать в menu.xml следующим образом:
shortcut="${sales.menu.customer}"
По умолчанию - NEW_TAB.
Значение NEW_WINDOW поддерживается только в Desktop Client, в Web Client оно эквивалентно NEW_TAB.
Атрибуты insertBefore, insertAfter для элемента item не поддерживаются в Studio. Поэтому если вы задали эти атрибуты вручную, не открывайте дизайнер меню Studio, иначе они будут удалены.
По умолчанию главное меню не влияет на возможность изменения размера диалоговых окон.
Элементы item:
Атрибуты param:
Данный элемент должен содержать вложенные элементы permission, каждый из которых описывает одно требуемое разрешение. Пункт меню доступен только при наличии всех требуемых разрешений.
Атрибуты permission:
Пример файла меню:
<menu-config xmlns="http://schemas.haulmont.com/cuba/menu.xsd">
<menu id="sales" insertBefore="administration">
<item id="sales$Customer.lookup"/>
<separator/>
<item id="sales$Order.lookup"/>
</menu>
</menu-config>
Локализованное название элемента меню формируется следующим образом: к идентификатору элемента добавляется префикс menu-config с точкой на конце, и полученная строка используется как ключ в главном пакете сообщений. Например:
menu-config.sales=Sales
menu-config.sales$Customer.lookup=Customers
Если атрибут id не задан, имя элемента меню будет составлено из имени класса (если задан атрибут class) или имени бина и его метода (если задан атрибут bean), поэтому для локализации рекомендуется указывать атрибут id.
Файлы данного типа используются для регистрации кастомных типов данных и неперсистентных сущностей, и для задания мета-аннотаций.
Схема XML доступна по адресу http://schemas.haulmont.com/cuba/6.10/metadata.xsd.
Расположение файла metadata.xml задается в свойстве приложения cuba.metadataConfig.
Рассмотрим структуру файла.
metadata - корневой элемент.
Элементы metadata:
Элементы datatypes:
Элемент datatype может также содержать другие атрибуты, зависящие от конкретного класса реализации Datatype.
Должен иметь атрибут class, задающий класс реализации. Другие атрибуты опциональны и зависят от реализации. См. Пример специализированного Datatype.
Атрибуты metadata-model:
Элементы metadata-model:
Элемент annotations содержит список элементов entity, которые определяют сущности, для которых задаются мета-аннотации. Каждый элемент entity должен содержать атрибут class, который задает класс сущности, и список элементов annotation.
Элемент annotation определяет мета-аннотацию. Он имеет атрибут name, соответствующий имени мета-аннотации. Мэп атрибутов мета-аннотации задается списком вложенных элементов attribute.
Пример:
<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
<metadata-model root-package="com.sample.sales">
<class>com.sample.sales.entity.SomeNonPersistentEntity</class>
<class>com.sample.sales.entity.OtherNonPersistentEntity</class>
</metadata-model>
<annotations>
<entity class="com.haulmont.cuba.security.entity.User">
<annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory">
<attribute name="value" value="true" datatype="boolean"/>
</annotation>
<annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore">
<attribute name="value" value="true" datatype="boolean"/>
</annotation>
</entity>
<entity class="com.haulmont.cuba.core.entity.Category">
<annotation name="com.haulmont.cuba.core.entity.annotation.SystemLevel">
<attribute name="value" value="false" datatype="boolean"/>
</annotation>
</entity>
</annotations>
</metadata>
Файлы данного типа используются в блоках Web Client и Desktop Client для регистрации специфических разрешений пользователей.
Расположение файла задается в свойстве приложения cuba.permissionConfig. При создании нового проекта в Studio, она создает файл web-permissions.xml в корневом пакете модуля web, например modules/web/src/com/company/sample/web-permissions.xml.
Схема XML доступна по адресу http://schemas.haulmont.com/cuba/6.10/permissions.xsd.
Рассмотрим структуру файла.
permission-config - корневой элемент.
Элементы permission-config:
Элементы specific:
Пример:
<permission-config xmlns="http://schemas.haulmont.com/cuba/permissions.xsd">
<specific>
<category id="app">
<permission id="app.doSomething"/>
<permission id="app.doSomethingOther"/>
</category>
</specific>
</permission-config>
Файлы данного типа являются стандартными для JPA и используются для регистрации персистентных сущностей и задания параметров функционирования фреймворка ORM.
Расположение файла persistence.xml задается в свойстве приложения cuba.persistenceConfig.
На старте блока Middleware из заданных файлов собирается один persistence.xml и сохраняется в рабочем каталоге приложения. Параметры ORM могут переопределяться каждым следующим файлом списка, поэтому порядок указания файлов важен.
Пример файла:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
<persistence-unit name="sales" transaction-type="RESOURCE_LOCAL">
<class>com.sample.sales.entity.Customer</class>
<class>com.sample.sales.entity.Order</class>
</persistence-unit>
</persistence>
Файлы данного типа определяют конфигурацию дополнительного контейнера Spring Framework для блока Middleware, который предназначен для экспорта сервисов и других компонентов среднего слоя, доступных клиентскому уровню (далее контейнер удаленного доступа).
Расположение файла remoting-spring.xml задается в свойстве приложения cuba.remotingSpringContextConfig.
Контейнер удаленного доступа создается таким образом, что основной контейнер (конфигурируемый файлами spring.xml) является родительским по отношению к нему. Это означает, что бины контейнера удаленного доступа могут обращаться к бинам основного контейнера, а бины основного контейнера "не видят" контейнер удаленного доступа.
Основная задача контейнера удаленного доступа - сделать сервисы Middleware доступными клиентскому уровню с помощью механизма Spring HttpInvoker. Для этого в cuba-remoting-spring.xml базового проекта cuba определяется бин servicesExporter типа RemoteServicesBeanCreator, который получает из основного контейнера все классы сервисов и экспортирует их. В дополнение к обычным аннотированным сервисам контейнер удаленного доступа экспортирует некоторые специфические бины, такие как LoginService.
Кроме того, cuba-remoting-spring.xml определяет базовый пакет, начиная с которого производится поиск аннотированных классов контроллеров Spring MVC, используемых для загрузки-выгрузки файлов.
В прикладном проекте определять файл типа remoting-spring.xml необходимо только в том случае, если создаются специфические контроллеры Spring MVC. Сервисы прикладного проекта в любом случае будут импортированы стандартным бином servicesExporter, определенным в компоненте cuba платформы.
Файлы данного типа используются в блоках Web Client и Desktop Client, реализующих универсальный пользовательский интерфейс, для регистрации XML-дескрипторов экранов.
Схема XML доступна по адресу http://schemas.haulmont.com/cuba/6.10/screens.xsd.
Расположение файла задается в свойстве приложения cuba.windowConfig. При создании нового проекта в Studio, она создает файл web-screens.xml в корневом пакете модуля web, например modules/web/src/com/company/sample/web-screens.xml.
Рассмотрим структуру файла.
screen-config - корневой элемент. Он содержит следующие элементы:
Атрибуты screen:
В случае Callable метод call() должен возвращать экземпляр созданного Window, который будет возвращен вызывающему коду как результат WindowManager.openWindow(). Класс может содержать конструктор с параметрами для передачи ему строковых значений, заданных вложенным элементом param (см. ниже).
Элементы screen:
Атрибуты param:
Атрибуты include:
Пример файла screens.xml:
<screen-config xmlns="http://schemas.haulmont.com/cuba/screens.xsd">
<screen id="sales$Customer.lookup" template="/com/sample/sales/gui/customer/customer-browse.xml"/>
<screen id="sales$Customer.edit" template="/com/sample/sales/gui/customer/customer-edit.xml"/>
<screen id="sales$Order.lookup" template="/com/sample/sales/gui/order/order-browse.xml"/>
<screen id="sales$Order.edit" template="/com/sample/sales/gui/order/order-edit.xml"/>
</screen-config>
Файлы данного типа определяют конфигурацию основного контейнера Spring Framework для каждого блока приложения.
Расположение файла spring.xml задается в свойстве приложения cuba.springContextConfig.
Основная часть конфигурирования контейнера возложена на аннотации бинов (такие как @Component, @Service, @Inject и др.), поэтому обязательной частью spring.xml в прикладном проекте является только элемент context:component-scan, в котором задается базовый пакет Java, с которого начинается поиск аннотированных классов. Например:
<context:component-scan base-package="com.sample.sales"/>
Остальное содержимое зависит от того, для какого блока приложения конфигурируется контейнер: например, для Middleware это регистрация JMX-бинов, для блоков клиентского уровня - импорт сервисов.
Файлы данного типа используются для описания представлений, см. Представления.
Схема XML доступна по адресу http://schemas.haulmont.com/cuba/6.10/view.xsd.
views - корневой элемент
Элементы views:
Атрибуты view:
Элементы view:
Атрибуты property:
Элементы property:
Атрибуты include:
Пример:
<views xmlns="http://schemas.haulmont.com/cuba/view.xsd">
<view class="com.sample.sales.entity.Order"
name="order-with-customer"
extends="_local">
<property name="customer" view="_minimal"/>
</view>
<view class="com.sample.sales.entity.Item"
name="itemsInOrder">
<property name="quantity"/>
<property name="product" view="_minimal"/>
</view>
<view class="com.sample.sales.entity.Order"
name="order-with-customer-defined-inline"
extends="_local">
<property name="customer">
<property name="name"/>
<property name="email"/>
</property>
</view>
</views>
См. также свойство приложения cuba.viewsConfig.
Файл web.xml является стандартным дескриптором веб-приложения Java EE, и должен быть создан для блоков Middleware, Web Client и Web Portal.
В проекте приложения файлы web.xml располагаются в каталогах web/WEB-INF соответствующих модулей.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- Application properties config files -->
<context-param>
<param-name>appPropertiesConfig</param-name>
<param-value>
classpath:com/company/sample/app.properties
/WEB-INF/local.app.properties
"file:${catalina.base}/conf/app-core/local.app.properties"
</param-value>
</context-param>
<!--Application components-->
<context-param>
<param-name>appComponents</param-name>
<param-value>com.haulmont.cuba com.haulmont.reports</param-value>
</context-param>
<listener>
<listener-class>com.haulmont.cuba.core.sys.AppContextLoader</listener-class>
</listener>
<servlet>
<servlet-name>remoting</servlet-name>
<servlet-class>com.haulmont.cuba.core.sys.remoting.RemotingServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>remoting</servlet-name>
<url-pattern>/remoting/*</url-pattern>
</servlet-mapping>
</web-app>
В элементах context-param задаются инициализирующие параметры объекта ServletContext данного веб-приложения. Список компонентов приложения задается в параметре appComponents, список файлов свойств приложения задается в параметре appPropertiesConfig.
В элементе listener задается класс слушателя, реализующего интерфейс ServletContextListener. В блоке Middleware CUBA-приложения в качестве слушателя должен использоваться класс AppContextLoader, выполняющий инициализацию AppContext.
Далее следуют определения сервлетов, среди которых обязательным для Middleware является класс RemotingServlet, связанный с контейнером удаленного доступа (см. remoting-spring.xml). Данный сервлет отображен на URL /remoting/*.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<context-param>
<description>Web resources version for correct caching in browser</description>
<param-name>webResourcesTs</param-name>
<param-value>${webResourcesTs}</param-value>
</context-param>
<!-- Application properties config files -->
<context-param>
<param-name>appPropertiesConfig</param-name>
<param-value>
classpath:com/company/sample/web-app.properties
/WEB-INF/local.app.properties
"file:${catalina.base}/conf/app/local.app.properties"
</param-value>
</context-param>
<!--Application components-->
<context-param>
<param-name>appComponents</param-name>
<param-value>com.haulmont.cuba com.haulmont.reports</param-value>
</context-param>
<listener>
<listener-class>com.vaadin.server.communication.JSR356WebsocketInitializer</listener-class>
</listener>
<listener>
<listener-class>com.haulmont.cuba.web.sys.WebAppContextLoader</listener-class>
</listener>
<servlet>
<servlet-name>app_servlet</servlet-name>
<servlet-class>com.haulmont.cuba.web.sys.CubaApplicationServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>com.haulmont.cuba.web.sys.CubaDispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>rest_api</servlet-name>
<servlet-class>com.haulmont.restapi.sys.CubaRestApiServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/dispatch/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>app_servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>rest_api</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
<filter>
<filter-name>cuba_filter</filter-name>
<filter-class>com.haulmont.cuba.web.sys.CubaHttpFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>cuba_filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>restSpringSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>contextAttribute</param-name>
<param-value>org.springframework.web.servlet.FrameworkServlet.CONTEXT.rest_api</param-value>
</init-param>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>springSecurityFilterChain</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>restSpringSecurityFilterChain</filter-name>
<url-pattern>/rest/*</url-pattern>
</filter-mapping>
</web-app>
В элементах context-param заданы списки компонентов приложения и файлов свойств приложения. Параметр webResourcesTs со значением, подставляемым во время сборки, обеспечивает корректное кэширование статических ресурсов в веб браузере.
В качестве ServletContextListener в блоке Web Client используется класс WebAppContextLoader.
JSR356WebsocketInitializer необходим для поддержки протокола WebSockets.
Сервлет CubaApplicationServlet обеспечивает функционирование универсального пользовательского интерфейса, основанного на фреймворке Vaadin.
Сервлет CubaDispatcherServlet инициализирует дополнительный контекст Spring для работы контроллеров Spring MVC. Этот контекст конфигурируется файлом dispatcher-spring.xml.
Сервлет CubaRestApiServlet обеспечивает функционирование универсального REST API.
Приложение B: Свойства приложения
В данном приложении в алфавитном порядке описаны доступные свойства приложения.
Задает имена дополнительных хранилищ данных, используемых в приложении.
Используется во всех стандартных блоках.
Пример:
cuba.additionalStores = db1, mem1
Разрешает универсальному фильтру использовать режим последовательного наложения фильтров. См. также Последовательная выборка.
Значение по умолчанию: true
Хранится в базе данных.
Интерфейс: GlobalConfig
Используется в блоках Web Client и Middleware.
Логин пользователя, от имени которого создается анонимная сессия (см. cuba.anonymousSessionId).
Значение по умолчанию: anonymous
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Задает UUID анонимной пользовательской сессии, которая доступна до логина пользователя. Данная сессия всегда создается автоматически на старте сервера. См. также cuba.anonymousLogin.
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
Включает режим выполнения скриптов БД сервером на старте приложения.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Список поддерживаемых языков интерфейса.
Формат свойства: {название_языка1}|{код_языка_1};{название_языка2}|{код_языка_2};… Пример:
cuba.availableLocales=French|fr;English|en
{название_языка} − это название, которое будет отображаться в списках доступных языков. Например, в окне входа в систему, в экране редактирования пользователя.
{код_языка} − соответствует коду, возвращаемому методом Locale.getLanguage(). Используется как суффикс для формирования имен файлов пакетов сообщений. Например, messages_fr.properties.
Следует иметь в виду, что язык, который указан первым в списке языков свойства cuba.availableLocales, будет отображаться первым в списке доступных языков в том случае, если среди языков данного свойства не будет найден текущий язык операционной системы пользователя. Если же язык операционной системы присутствует в списке доступных, то отображаться первым будет он.
Значение по умолчанию: English|en;Russian|ru;French|fr
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
cuba.backgroundWorker.maxActiveTasksCount
Максимальное количество активных фоновых задач.
Значение по умолчанию: 100
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.backgroundWorker.timeoutCheckInterval
Задает интервал (в миллисекундах) проверки таймаутов фоновых задач.
Значение по умолчанию: 5000
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.bruteForceProtection.enabled
Включает механизм защиты от взлома пароля методом перебора.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
cuba.bruteForceProtection.blockIntervalSec
Задает интервал блокировки пользователя в секундах после превышения максимального числа неуспешных попыток входа, если свойство cuba.bruteForceProtection.enabled включено.
Значение по умолчанию: 60
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
cuba.bruteForceProtection.maxLoginAttemptsNumber
Максимальное количество неуспешных попыток входа для пары логин + IP-адрес, если свойство cuba.bruteForceProtection.enabled включено.
Значение по умолчанию: 5
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Включает взаимодействие серверов Middleware в кластере. Подробнее см. Настройка взаимодействия серверов Middleware.
Значение по умолчанию: false
Используется в блоке Middleware.
Путь к конфигурационному файлу JGroups. Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Пример:
cuba.cluster.jgroupsConfig = my_jgroups_tcp.xml
Значение по умолчанию: jgroups.xml
Используется в блоке Middleware.
cuba.cluster.messageSendingQueueCapacity
Ограничивает размер очереди сообщений кластера middleware. Если очередь переполняется, новые сообщения отбрасываются.
Значение по умолчанию: Integer.MAX_VALUE
Используется в блоке Middleware.
cuba.cluster.stateTransferTimeout
Задаёт таймаут в миллисекундах для получения состояний кластера middleware при запуске.
Значение по умолчанию: 10000
Используется в блоке Middleware.
Конфигурационный параметр, задающий расположение каталога конфигурации данного блока приложения.
Значение по умолчанию для быстрого развертывания в Tomcat: ${catalina.home}/conf/${cuba.webContextName}, что означает подкаталог с именем веб-приложения в каталоге tomcat/conf, например tomcat/conf/app-core.
Значение по умолчанию для WAR и UberJAR: ${app.home}/${cuba.webContextName}/conf, что означает расположение в подкаталоге домашнего каталога приложения.
Значение по умолчанию для блока Desktop Client: ${cuba.desktop.home}/conf.
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
Задает таймаут подключения клиентского блока к Middleware. Неотрицательное значение передается в метод setReadTimeout() класса URLConnection.
См. также cuba.connectionTimeout.
Значение по умолчанию: -1
Используется в блоках Web Client, Web Portal, Desktop Client.
Задает таймаут подключения клиентского блока к Middleware. Неотрицательное значение передается в метод setConnectTimeout() класса URLConnection.
См. также cuba.connectionReadTimeout.
Значение по умолчанию: -1
Используется в блоках Web Client, Web Portal, Desktop Client.
Задает список URL для подключения клиентских блоков к серверам Middleware.
Значением свойства должен быть один или несколько разделенных запятой URL вида http[s]://host[:port]/app-core, где host - имя сервера, port - порт сервера, app-core - имя веб-приложения, реализующего блок Middleware. Например:
cuba.connectionUrlList=http://localhost:8080/app-core
В случае использования кластера серверов Middleware, для обеспечения отказоустойчивости и балансировки нагрузки необходимо перечислить их адреса через запятую:
cuba.connectionUrlList=http://server1:8080/app-core,http://server2:8080/app-core
Подробнее см. Настройка обращения к кластеру Middleware.
См. также свойство cuba.useLocalServiceInvocation.
Интерфейс: ClientConfig
Используется в блоках Web Client, Web Portal, Desktop Client.
Аддитивное свойство, задающее файл credits.xml, содержащий информацию об используемом программном обеспечении.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется в блоках Web Client и Desktop Client.
Пример:
cuba.creditsConfig = +com/company/base/credits.xml
cuba.crossDataStoreReferenceLoadingBatchSize
Размер пакета, применямого в DataManager для загрузки ссылок из другого хранилища.
Значение по умолчанию: 50
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
cuba.dataManagerChecksSecurityOnMiddleware
Указывает, что DataManager должен применять подсистемы безопасности когда вызывается из кода Middleware.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Задает JNDI имя источника данных javax.sql.DataSource, через который производится обращение к базе данных приложения.
Значение по умолчанию: java:comp/env/jdbc/CubaDS
Используется в блоке Middleware.
Конфигурационный параметр, задающий расположение рабочего каталога данного блока приложения.
Значение по умолчанию для быстрого развертывания в Tomcat: ${catalina.home}/work/${cuba.webContextName}, что означает подкаталог с именем веб-приложения в каталоге tomcat/work, например tomcat/work/app-core.
Значение по умолчанию для WAR и UberJAR: ${app.home}/${cuba.webContextName}/work, что означает расположение в подкаталоге домашнего каталога приложения.
Значение по умолчанию для блока Desktop Client: ${cuba.desktop.home}/work.
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
Конфигурационный параметр, задающий расположение каталога скриптов базы данных.
Значение по умолчанию для быстрого развертывания в Tomcat: ${catalina.home}/webapps/${cuba.webContextName}/WEB-INF/db, что означает расположение в подкаталоге WEB-INF/db веб-приложения в Tomcat.
Значение по умолчанию для WAR и UberJAR: web-inf:db, что означает расположение в подкаталоге WEB-INF/db внутри WAR или UberJAR.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Задает тип используемой базы данных. Совместно с cuba.dbmsVersion влияет на выбор имплементаций интерфейсов интеграции с СУБД и на поиск скриптов создания и обновления БД.
Подробнее см. Типы СУБД.
Значение по умолчанию: hsql
Используется в блоке Middleware.
Необязательное свойство, задающее версию используемой базы данных. Совместно с cuba.dbmsType влияет на выбор имплементаций интерфейсов интеграции с СУБД и на поиск скриптов создания и обновления БД.
Подробнее см. Типы СУБД.
Значение по умолчанию: отсутствует
Используется в блоке Middleware.
cuba.defaultPermissionValuesConfig
Определяет набор файлов, описывающих разрешения пользователя по умолчанию. Разрешения по умолчанию используются тогда, когда ни одна из имеющихся ролей не задаёт разрешения на конкретный экран или функциональность. Разрешения необходимы по большей части для запрещающих ролей, подробнее см. default-permission-values.xml.
Значение по умолчанию: cuba-default-permission-values.xml
Используется в блоке Middleware.
Пример:
cuba.defaultPermissionValuesConfig = +my-default-permission-values.xml
Задает таймаут транзакции по умолчанию.
Значение по умолчанию: 0, означает, что таймаут отсутствует.
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Включает корректировку времени, выдаваемого интерфейсом TimeSource блока DesktopClient - оно становится примерно равным времени Middleware, к которому подключен данный клиент.
Значение по умолчанию: true
Интерфейс: DesktopConfig
Используется в блоке DesktopClient.
cuba.desktop.useServerTimeZone
Устанавливает в JVM блока DesktopClient timezone Middleware, к которому подключен данный клиент.
Значение по умолчанию: true
Интерфейс: DesktopConfig
Используется в блоке DesktopClient.
cuba.disableEscapingLikeForDataStores
Содержит список хранилищ данных, для которых запрещён оператор ESCAPE в JPQL-запросах, содержащих LIKE, в фильтрах.
Хранится в базе данных.
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
Запрещает автоматическую генерацию файла orm.xml для расширенных сущностей.
Значение по умолчанию: false, означает что orm.xml будет создан автоматически при наличии расширенных сущностей.
Используется в блоке Middleware.
cuba.dispatcherSpringContextConfig
Аддитивное свойство, задающее файл dispatcher-spring.xml в клиентских блоках.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется в блоках Web Client, Web Portal.
Пример:
cuba.dispatcherSpringContextConfig = +com/company/sample/portal-dispatcher-spring.xml
Задает список каталогов, из которых можно загружать с Middleware файлы через com.haulmont.cuba.core.controllers.FileDownloadController. Загрузка файлов используется в частности механизмом отображения журналов сервера, доступным через экран Администрирование → Журнал сервера веб-клиента.
Список задается через ";".
Значение по умолчанию: ${cuba.tempDir};${cuba.logDir}, означает что файлы можно загружать из временного каталога и каталога логов.
Используется в блоке Middleware.
Параметры отправки email, подробно описаны в Настройка параметров отправки email.
Задает корни структуры каталогов файлового хранилища. Подробнее см. Стандартная реализация хранилища
Значение по умолчанию: null
Интерфейс: ServerConfig
Используется в блоке Middleware.
cuba.enableDeleteStatementInSoftDeleteMode
Переключатель для обратной совместимости. При установке в true, позволяет выполнять оператор JPQL delete from для soft-deleted сущностей при включенном режиме мягкого удаления. Такой оператор трансформируется в SQL, который удаляет экземпляры не помеченные на мягкое удаление. Это неинтуитивное поведение по умолчанию запрещено.
Значение по умолчанию: false
Используется в блоке Middleware.
cuba.enableSessionParamsInQueryFilter
Переключатель для обратной совместимости. При установке в false условия в фильтре запросов источника данных и компонента Filter будут применяться только после передачи как минимум одного значения параметра, а параметры сессии работать не будут.
Значение по умолчанию: true
Используется в блоке Web Client.
cuba.entityAttributePermissionChecking
При установке в true включает проверку прав на атрибуты сущностей на уровне Middleware. Если значением является false, права на атрибуты проверяются только на клиентском уровне, т.е. в Generic UI и REST API.
Значение по умолчанию: false
Хранится в базе данных.
Используется в блоке Middleware.
Активирует механизм журналирования сущностей.
Значение по умолчанию: true
Хранится в базе данных.
Интерфейс: EntityLogConfig
Используется в блоке Middleware.
cuba.groovyEvaluationPoolMaxIdle
Задает максимальное число неиспользуемых скомпилированных выражений Groovy в пуле при выполнении метода Scripting.evaluateGroovy(). Данный параметр рекомендуется увеличивать при потребности в интенсивном исполнении выражений Groovy, например, вследствие большого количества папок приложения.
Значение по умолчанию: 8
Используется во всех стандартных блоках.
Задает список классов, импортируемых всеми выполняемыми через Scripting выражениями на Groovy.
Имена классов в списке разделяются запятой или точкой с запятой.
Значение по умолчанию: com.haulmont.cuba.core.global.PersistenceHelper
Используется во всех стандартных блоках.
Пример:
cuba.groovyEvaluatorImport=com.haulmont.cuba.core.global.PersistenceHelper,com.abc.sales.CommonUtils
cuba.gui.genericFilterChecking
Оказывает влияние на поведение компонента Filter.
При установке в true пользователь не может применить фильтр, не введя ни одного параметра.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.genericFilterColumnsCount
Определяет количество колонок для размещения условий фильтра.
Значение по умолчанию: 3
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.genericFilterConditionsLocation
Определяет положение панели условий фильтра. Доступны два положения: top (над элементами управления фильтром) и bottom (под элементами управления фильтром).
Значение по умолчанию: top
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.genericFilterControlsLayout
Задает шаблон расположения элементов компонента Filter. Каждый элемент имеет следующий формат: [component_name | options-comma-separated], например [pin | no-caption, no-icon].
Доступные элементы:
Следующие действия могут быть опциями элемента settings: save, save_as, edit, remove, pin, make_default, save_search_folder, save_app_folder, clear_values.
Они также могут быть использованы и как независимые элементы компоновки. В этом случае они могут иметь следующие опции:
Значение по умолчанию:
++[filters_popup] [add_condition] [spacer] \
[settings | save, save_as, edit, remove, make_default, pin, save_search_folder, save_app_folder, clear_values] \
[max_results] [fts_switch]++
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.genericFilterManualApplyRequired
Оказывает влияние на поведение компонента Filter.
При установке в true экраны, содержащие фильтры, не будут автоматически загружать соответствующие источники данных до тех пор, пока пользователь не нажмет кнопку Применить фильтра.
При открытии экрана списка с помощью папки приложения или папки поиска значение cuba.gui.genericFilterManualApplyRequired не учитывается, то есть в этом случае фильтр будет применяться. Фильтр не применится, если значение атрибута applyDefault у папки явно установлено в false.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.genericFilterMaxResultsOptions
Задает возможные значения списка Show rows компонента Filter.
Значение NULL указывает, что список должен содержать пустое значение.
Значение по умолчанию: NULL, 20, 50, 100, 500, 1000, 5000
Интерфейс: ClientConfig
Хранится в базе данных.
Используется в блоках Web Client и Desktop Client.
cuba.gui.genericFilterPopupListSize
Определяет число элементов, отображающихся в выпадающем списке кнопки Search. Если количество фильтров превышает значение, к выпадающему списку добавляется действие Show more…. Действие открывает новое диалоговое окно со списком всех доступных фильтров.
Значение по умолчанию: 10
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.genericFilterTrimParamValues
Определяет, нужно ли обрезать пробелы в начале и конце строки текстового поиска. Если установлено false, введённые строки будут использоваться без обрезки.
Значение по умолчанию: true
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.layoutAnalyzerEnabled
Позволяет отключить команду анализа компоновки экрана Analyze layout, доступную в контекстном меню вкладок главного окна и в заголовках модальных окон.
Значение по умолчанию: true
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.lookupFieldPageLength
Задает количество опций на одной странице выпадающего списка в компонентах LookupField и LookupPickerField. Может быть переопределено для конкретного экземпляра компонента с помощью XML-атрибута pageLength.
Значение по умолчанию: 10
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоке Web Client.
cuba.gui.manualScreenSettingsSaving
Если установлено в true, экраны не будут сохранять свои настройки автоматически при закрытии. В этом режиме пользователь может сохранить или сбросить настройки экрана, используя контекстное меню на вкладке экрана или на заголовке диалогового окна.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.showIconsForPopupMenuActions
Включает отображение значков действий в пунктах контекстного меню Table и PopupButton.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоке Web Client.
cuba.gui.systemInfoScriptsEnabled
Разрешает показ SQL-скриптов добавления/изменения/извлечения экземпляра сущности в окне System Information.
Данные скрипты фактически показывают содержимое строк базы данных, хранящих выбранный экземпляр сущности, независимо от настроек безопасности, в которых некоторые атрибуты могут быть запрещены. Поэтому рекомендуется либо отобрать право на CUBA / Generic UI / System Information для всех ролей пользователей, кроме администраторов, либо установить свойство cuba.gui.systemInfoScriptsEnabled для всего приложения в false.
Значение по умолчанию: true
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
Определяет форму диалога, возникающего при попытке закрытия экрана, имеющего несохраненные изменения в источниках данных.
Значение true задает форму с тремя вариантами выбора: сохранить изменения, не сохранять, либо не закрывать экран.
Значение false задает форму с двумя вариантами: закрыть экран не сохраняя изменений, либо не закрывать экран.
Значение по умолчанию: true
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.gui.validationNotificationType
Задаёт тип уведомления об ошибке валидации стандартного окна.
Значением может быть элемент перечисления com.haulmont.cuba.gui.components.Frame.NotificationType:
Значение по умолчанию: TRAY.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
cuba.hasMultipleTableConstraintDependency
Позволяет использовать стратегию наследования JOINED для композитных сущностей. Если установлено значение true, платформа обеспечит нужный порядок вставки новых сущностей в базу данных.
Значение по умолчанию: false
Задает текст, возвращаемый запросом на health check URL.
Значение по умолчанию: ok
Интерфейс: GlobalConfig
Используется во всех блоках приложения за исключением Desktop Client.
cuba.httpSessionExpirationTimeoutSec
Задает таймаут бездействия HTTP-сессии в секундах.
Значение по умолчанию: 1800
Интерфейс: WebConfig
Используется в блоке Web Client.
|
Рекомендуется выставлять параметры cuba.userSessionExpirationTimeoutSec и cuba.httpSessionExpirationTimeoutSec в одинаковое значение. |
Аддитивное свойство, задающее наборы значков.
Используется в блоках Web Client и Desktop Client.
Пример использования:
cuba.iconsConfig = +com.company.demo.web.MyIconSet
Для SSO Identity Provider запрещает доступ к IDP HTTP cookie из JavaScript.
Значение по умолчанию: true (доступ из JS запрещен)
Интерфейс: IdpConfig
Используется в блоке Web Client.
Для SSO Identity Provider устанавливает время жизни IDP HTTP cookie в секундах.
Значение по умолчанию: 31536000 (~1 год)
Интерфейс: IdpConfig
Используется в блоке Web Client.
cuba.idp.serviceProviderLogoutUrls
Для SSO Identity Provider устанавливает список URL, которые используются для уведомления SP о логауте или истечении сессии пользователей. Значения перечисляются через запятую.
Например:
cuba.idp.serviceProviderLogoutUrls = http://foo:8081/app/dispatch/idpc/logout,http://bar:8082/app/dispatch/idpc/logout
Интерфейс: IdpConfig
Используется в блоке Web Client.
Для SSO Identity Provider устанавливает список URL сервис-провайдеров. Значения перечисляются через запятую. Символ '/' в конце URL обязателен.
Например:
cuba.idp.serviceProviderUrls = http://foo:8081/app/,http://bar:8082/app/
Интерфейс: IdpConfig
Используется в блоке Web Client.
cuba.idp.serviceProviderUrlMasks
Для SSO Identity Provider устанавливает маски разрешенных URL сервис-провайдеров в формате Java Regex. Значения перечисляются через запятую. Позволяет корректно обрабатывать внешние ссылки, когда пользователь не авторизован в системе.
Рекомендуется внимательно прописывать маски, чтобы избежать переадресации на сомнительные источники, поэтому символ '/' в конце URL обязателен.
Пример:
cuba.idp.serviceProviderUrlMasks = http://your-foo.com/.*,http://your-bar.com/.*
Интерфейс: IdpConfig
Используется в блоке Web Client.
cuba.idp.sessionExpirationTimeoutSec
Для SSO Identity Provider устанавливает таймаут неактивности сессий IDP в секундах.
Значение по умолчанию: 18000 (5 часов)
Интерфейс: IdpConfig
Используется в блоке Web Client.
cuba.idp.sessionExpirationCheckIntervalMs
Для SSO Identity Provider устанавливает интервал проверки неактивности сессий IDP в миллисекундах.
Значение по умолчанию: 30000 (30 сек)
Интерфейс: IdpConfig
Используется в блоке Web Client.
cuba.idp.standardAuthenticationUsers
Разделенный запятыми список логинов пользователей, которые могут входить в систему, используя только стандартную аутентификацию. Для этих пользователей внешняя аутентификация (IDP SSO) запрещена. См. также cuba.web.standardAuthenticationUsers.
Пустой список означает, что все могут использовать внешнюю аутентификацию, если она включена.
Пример использования:
cuba.web.standardAuthenticationUsers = admin
Значение по умолчанию: <empty list>
Интерфейс: IdpAuthConfig
Используется в блоке Web Client.
cuba.idp.ticketExpirationTimeoutSec
Для SSO Identity Provider устанавливает таймаут тикетов SSO в секундах.
Значение по умолчанию: 180 (3 мин)
Интерфейс: IdpConfig
Используется в блоке Web Client.
cuba.idp.trustedServicePassword
Для SSO Identity Provider устанавливает пароль, используемый в коммуникации server-to-server между SP и IDP.
Интерфейс: IdpConfig
Используется в блоке Web Client.
Включает режим фильтрации дубликатов записей в памяти, вместо select distinct на уровне базы данных. Используется в DataManager.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Логин пользователя, под которым выполняется вход в систему при системной аутентификации.
Значение по умолчанию: admin
Используется в блоке Middleware.
cuba.keyForSecurityTokenEncryption
Используется в качестве ключа AES-шифрования токена безопасности (security token). Токен посылается внутри экземпляра сущности, когда он загружается со среднего слоя в следующих случаях:
Хотя токен не содержит значений никаких атрибутов (только имена атрибутов и идентификаторы отфильтрованных сущностей), рекомендуется изменить значение по умолчанию при развертывании.
Значение по умолчанию: CUBA.Platform
Интерфейс: ServerConfig
Используется в блоке Middleware.
Когда в памяти приложения с помощью метода Metadata.create() создается экземпляр сущности, унаследованной от BaseLongIdEntity или BaseIntegerIdEntity, ему сразу присваивается идентификатор. Значение идентификатора получается из механизма, который извлекает следующее число из последовательности в базе данных. Для того, чтобы уменьшить количество обращений к среднему слою и к БД, инкремент последовательности устанавливается по умолчанию в 100, что означает что фреймворк на самом деле получает диапазон значений при каждом обращении к БД. Этот диапазон "кэшируется" и механизм выдает значения идентификаторов без обращений к БД, пока не исчерпается диапазон.
Данное свойство задает инкремент последовательностей и соответствующий размер кэшированного диапазона в памяти.
|
Если вы меняете значение данного свойства когда в БД уже хранятся сущности, необходимо также пересоздать имеющиеся последовательности с новым инкрементом (равным cuba.numberIdCacheSize) и начальными значениями, соответствующими максимальным имеющимся идентификаторам. Не забудьте установить значение свойства на всех блоках, используемых в приложении. Например, если у вас есть Web Client, Portal Client и Middleware, нужно установить одинаковое значение в web-app.properties, portal-app.properties и app.properties. |
Значение по умолчанию: 100
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
Включает или отключает возможность пользователя выбирать язык интерфейса при входе в систему.
Если cuba.localeSelectVisible=false, то локаль пользовательской сессии выбирается следующим образом:
Значение по умолчанию: true
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
Конфигурационный параметр, задающий расположение каталога журналов данного блока приложения.
Значение по умолчанию для быстрого развертывания: ${catalina.home}/logs, что означает каталог tomcat/logs.
Значение по умолчанию для WAR и UberJAR: ${app.home}/logs, что означает расположение в подкаталоге logs домашнего каталога приложения.
Значение по умолчанию для блока Desktop Client: ${cuba.desktop.home}/logs.
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
Аддитивное свойство, задающее главный пакет сообщений данного блока приложения.
Значением свойства может быть либо один пакет, либо список пакетов, разделенный пробелами.
Используется во всех стандартных блоках.
Пример:
cuba.mainMessagePack = +com.company.sample.gui com.company.sample.web
Максимальный размер файла в мегабайтах, который может быть загружен с помощью компонентов FileUploadField и FileMultiUploadField.
Значение по умолчанию: 20
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках Web Client и Desktop Client.
Аддитивное свойство, задающее файл menu.xml.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется в блоках Web Client и Desktop Client.
Пример:
cuba.menuConfig = +com/company/sample/web-menu.xml
Аддитивное свойство, задающее файл metadata.xml.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется в блоках Middleware, Web Client и Desktop Client.
Пример:
cuba.metadataConfig = +com/company/sample/metadata.xml
Задает имя бина, используемого для хэширования паролей пользователей.
Значение по умолчанию: cuba_Sha1EncryptionModule
Используется во всех стандартных блоках.
cuba.passwordPolicyEnabled
Определяет, нужно ли применять политику проверки пароля. Если свойство имеет значение true, то все новые задаваемые пользователями пароли будут проверяться в соответствии со свойством cuba.passwordPolicyRegExp.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ClientConfig
Используется в блоках клиентского уровня: Web Client, Web Portal, Desktop Client.
В данном свойстве задается регулярное выражение, которое используется в политике проверки пароля.
Значение по умолчанию:
((?=.*\\d)(?=.*\\p{javaLowerCase}) (?=.*\\p{javaUpperCase}).{6,20})
Это означает, что в пароль должен содержать от 6 до 20 символов, в нем можно использоваться цифры, символы и буквы латинского алфавита. При этом обязательно в пароле должна быть хотя бы одна цифра, одна буква в нижнем регистре и одна буква в верхнем регистре. Более подробную информацию о синтаксисе регулярных выражений можно найти на сайтах: https://ru.wikipedia.org/wiki/Регулярные_выражения и http://docs.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html
Интерфейс: ClientConfig
Хранится в базе данных.
Используется в блоках клиентского уровня: Web Client, Web Portal, Desktop Client.
Должно быть установлено в true, когда приложение выполняет тесты производительности.
Интерфейс: GlobalConfig
Значение по умолчанию: false
Используется в блоках Web Client и Middleware.
Аддитивное свойство, задающее файл permissions.xml.
Используется в блоках Web Client и Desktop Client.
Пример:
cuba.permissionConfig = +com/company/sample/web-permissions.xml
Аддитивное свойство, задающее файл persistence.xml.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется в блоках Middleware, Web Client и Desktop Client.
Пример:
cuba.persistenceConfig = +com/company/sample/persistence.xml
cuba.portal.anonymousUserLogin
Логин пользователя системы, который используется для создания анонимной пользовательской сессии в блоке Web Portal.
Пользователь с таким логином должен быть создан в подсистеме безопасности, и ему должны быть назначены соответствующие права. Пароль пользователя игнорируется, так как анонимная сессия портала создается методом loginTrusted() с передачей пароля, указанного в свойстве cuba.trustedClientPassword.
Интерфейс: PortalConfig
Используется в блоке Web Portal.
При установке в false отключает кэш запросов.
Значение по умолчанию: true
Интерфейс: QueryCacheConfig
Используется в блоке Middleware.
Максимальное количество записей в кэше запросов. Запись кэша определяется текстом запроса, параметрами запроса, параметрами пейджинга и признаком мягкого удаления.
Когда размер кэша приближается к максимальному, кэш удаляет записи, которые наименее вероятно будут использованы в дальнейшем.
Значение по умолчанию: 100
Интерфейс: QueryCacheConfig
Используется в блоке Middleware.
cuba.remotingSpringContextConfig
Аддитивное свойство, задающее файл remoting-spring.xml в блоке Middleware.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется в блоке Middleware.
Пример:
cuba.remotingSpringContextConfig = +com/company/sample/remoting-spring.xml
Задает список хостов, которым разрешен доступ к REST API. Значения хостов должны быть разделены запятой.
Значение по умолчанию: *
Используется в блоках Web Client и Web Portal.
Разрешает анонимный доступ к REST API.
Значение по умолчанию: false
Используется в блоках Web Client и Web Portal.
Задает идентификатор клиента REST API. Клиент - это не пользователь платформы, а приложение (какой-либо веб-портал или мобильный клиент), использующий REST API. Идентификатор и пароль клиента используются для базовой аутентификации при доступе к URL для получения токена.
Значение по умолчанию: client
Используется в блоках Web Client и Web Portal.
cuba.rest.client.authorizedGrantTypes
Определяеет список типов авторизации (grant type), поддерживаемых клиентом по умолчанию. Для отключения поддержки refresh-токенов, удалите элемент refresh_token из значения свойства.
Значение по умолчанию: password,external,refresh_token
Используется в блоках Web Client и Web Portal.
Задает пароль клиента REST API. Клиент - это не пользователь платформы, а приложение (какой-либо веб-портал или мобильный клиент), использующий REST API. Идентификатор и пароль клиента используются для базовой аутентификации при доступе к URL для получения токена.
Значение по умолчанию: secret
Используется в блоках Web Client и Web Portal.
cuba.rest.client.tokenExpirationTimeSec
Задает время жизни access токена REST API в секундах для клиента по умолчанию.
Значение по умолчанию: 43200 (12 часов)
Используется в блоках Web Client и Web Portal.
cuba.rest.client.refreshTokenExpirationTimeSec
Задает время жизни refresh токена REST API в секундах для клиента по умолчанию.
Значение по умолчанию: 31536000 (365 дней)
Используется в блоках Web Client и Web Portal.
cuba.rest.deleteExpiredTokensCron
Задает выражение cron, определяющее расписание удаления истекших OAuth токенов из базы данных.
Значение по умолчанию: 0 0 3 * * ?
Используется в блоке Middleware.
cuba.rest.jsonTransformationConfig
Аддитивное свойство, задающее файл, содержащий конфигурацию JSON трансформеров, используемых REST API для случаев, когда клиентскому приложению требуются данные в формате определенной версии модели данных.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
XSD файла доступна по адресу http://schemas.haulmont.com/cuba/6.10/rest-json-transformations.xsd.
Значение по умолчанию: отсутствует
Пример:
cuba.rest.jsonTransformationConfig = +com/company/sample/json-transformations.xml
Используется в блоках Web Client и Web Portal.
Максимальный размер файла (в байтах), который может быть загружен с помощью REST API.
Значение по умолчанию: 20971520 (20 Mb)
Используется в блоках Web Client и Web Portal.
cuba.rest.optimisticLockingEnabled
Включает оптимистичную блокировку сущностей, реализующих интерфейс Versioned, если атрибут version передан в JSON.
Значение по умолчанию: false
Используется в блоках Web Client и Web Portal.
cuba.rest.requiresSecurityToken
Если установлен в true, то в JSON загружаемой из БД сущности включается дополнительный системный атрибут, и этот же атрибут ожидается от клиента при сохранении сущности. Подробнее см. Ограничения для атрибутов-коллекций.
Значение по умолчанию: false
Используется в блоках Web Client и Web Portal.
Определяет, должен ли refresh-токен быть повторно использован. Если установить значение в false, то когда access-токен запрашивается с помощью refresh-токена, то будет выдан новый refresh-токен, а старый будет удалён.
Значение по умолчанию: true
Используется в блоках Web Client и Web Portal.
Аддитивное свойство задающее файл, который содержит список доступных для вызова через REST API сервисов.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
XSD файла доступна по адресу http://schemas.haulmont.com/cuba/6.10/rest-services-v2.xsd.
Значение по умолчанию: не задано
Example:
cuba.rest.servicesConfig = +com/company/sample/app-rest-services.xml
Используется в блоках Web Client и Web Portal.
Включает хранение OAuth токенов в базе данных. По умолчанию токены хранятся только в памяти.
Хранится в базе данных.
Интерфейс: ServerConfig
Значение по умолчанию: false
Используется в блоке Middleware.
Определяет, должны ли токены REST API быть маскированы в логах приложения.
Значение по умолчанию: true
Используется в блоках Web Client и Web Portal.
Аддитивное свойство задающее файл, который содержит список доступных для выполнения через REST API JPQL запросов.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
XSD файла доступна по адресу http://schemas.haulmont.com/cuba/6.10/rest-queries.xsd.
Значение по умолчанию: не задано
Example:
cuba.rest.queriesConfig = +com/company/sample/app-rest-queries.xml
Используется в блоках Web Client и Web Portal.
Включает и выключает механизм выполнения назначенных заданий CUBA.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Указывает имплементацию интерфейса Serialization, которая будет использоваться для сериализации объектов при их передаче между блоками приложения. Платформа содержит две имплементации:
Значение по умолчанию: com.haulmont.cuba.core.sys.serialization.StandardSerialization
Используется во всех стандартных блоках.
Аддитивное свойство, задающее файл spring.xml в каждом стандартном блоке приложения.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется во всех стандартных блоках.
Пример:
cuba.springContextConfig = +com/company/sample/spring.xml
Задает email, на который отправляются отчеты об исключениях из окна стандартного обработчика, и сообщения пользователей из экрана Help → Feedback.
Если данное свойство установлено в пустую строку, кнопка Report в окне обработчика исключений не показывается.
Для успешной отсылки email необходимо настроить параметры, описанные в разделе Настройка параметров отправки email
Значение по умолчанию: пустая строка.
Хранится в базе данных.
Интерфейс: WebConfig
Используется в блоке Web Client.
Конфигурационный параметр, задающий расположение временного каталога данного блока приложения.
Значение по умолчанию для быстрого развертывания в Tomcat: ${catalina.home}/temp/${cuba.webContextName}, что означает подкаталог с именем веб-приложения в каталоге tomcat/temp, например tomcat/temp/app-core.
Значение по умолчанию для WAR и UberJAR: ${app.home}/${cuba.webContextName}/temp, что означает расположение в подкаталоге домашнего каталога приложения.
Значение по умолчанию для блока Desktop Client: ${cuba.desktop.home}/temp.
Интерфейс: GlobalConfig
Используется во всех стандартных блоках.
Должно быть установлено в true, когда приложение выполняет автоматические UI-тесты.
Интерфейс: GlobalConfig
Значение по умолчанию: false
Используется в блоках Web Client, Desktop Client и Middleware.
Задает набор файлов *-theme.properties, в которых описаны переменные тем, такие как размеры диалоговых окон и ширина полей ввода по умолчанию.
Значением свойства должен быть список имен файлов, разделенный пробелами. Файлы загружаются по правилам интерфейса Resources.
Значение по умолчанию для Web Client: havana-theme.properties halo-theme.properties
Значение по умолчанию для Desktop Client: nimbus-theme.properties
Используется в блоках Web Client и Desktop Client.
Позволяет отключить обработку триггер-файлов вызова бинов.
Триггер-файл представляет собой файл, помещаемый в подкаталог triggers временного каталога данного блока приложения. Имя триггер-файла состоит из двух частей, разделенных точкой. Первая часть соответствует имени бина, вторая - имени вызываемого метода бина, например cuba_Messages.clearCache. Обработчик триггер-файлов следит за их появлением, вызывает соответствующие методы и удаляет файлы.
В платформе вызов обработчика задан в файле cuba-web-spring.xml, то есть по умолчанию обработка триггер-файлов производится для блока Web Client. На уровне проекта можно аналогично запустить обработку для других модулей, периодически вызывая метод process() бина cuba_TriggerFilesProcessor.
См. также свойство cuba.triggerFilesCheckInterval.
Значение по умолчанию: true
Используется в блоках, для которых настроена обработка, по умолчанию - Web Client.
cuba.triggerFilesCheckInterval
Устанавливает период в миллисекундах обработки триггер-файлов вызова бинов, заданный в файле cuba-web-spring.xml.
См. также свойство cuba.triggerFilesCheck.
Значение по умолчанию: 5000
Используется в блоке Web Client.
Пароль, используемый методом LoginService.loginTrusted(). Средний слой может аутентифицировать пользователей, подключающихся через доверенный клиентский блок, без проверки пользовательского пароля.
Это свойство используется в случае, если пароли пользователей не хранятся в БД, и реальную аутентификацию выполняет сам клиентский блок, например, путем интеграции с Active Directory.
Интерфейсы: ServerConfig, WebAuthConfig, PortalConfig
Используется в блоках: Middleware, Web Client, Web Portal.
cuba.trustedClientPermittedIpList
Список IP адресов, с которых возможен вызов метода LoginService.loginTrusted().
Значение по умолчанию: 127.0.0.1
Интерфейс: ServerConfig
Используется в блоке Middleware.
cuba.uniqueConstraintViolationPattern
Регулярное выражение, по которому определяется, что данное исключение произошло по причине нарушения ограничения уникальности в базе данных. Имя индекса, поддерживающего ограничение, будет взято из первой непустой группы выражения. Например:
ERROR: duplicate key value violates unique constraint "(.+)"
Имя индекса можно использовать для выдачи пользователю локализованного сообщения о том, для какой сущности нарушено ограничение. Для этого в главном пакете сообщений необходимо задать ключи, соответствующие именам индексов. Например:
IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists
Данное свойство позволяет настроить реакцию на исключения уникальности в зависимости от используемой версии и локали сервера базы данных.
Значение по умолчанию: возвращается методом PersistenceManagerService.getUniqueConstraintViolationPattern() для соответствующей СУБД.
Может быть определено в базе данных.
Используется во всех клиентских блоках приложения.
cuba.useCurrentTxForConfigEntityLoad
Если значение данного свойства true, то при загрузке экземпляров сущностей через конфигурационные интерфейсы будет использоваться текущая транзакция (если таковая имеется в данный момент), что может положительно сказаться на производительности. В противном случае всегда создается и завершается новая транзакция и возвращается detached экземпляр.
Значение по умолчанию: false
Используется в блоке Middleware.
cuba.useEntityDataStoreForIdSequence
Если данное свойство приложения установлено в true, последовательности для генерации идентификаторов наследников BaseLongIdEntity и BaseIntegerIdEntity будут создаваться в хранилище, к которому принадлежит данная сущность. В противном случае они создаются в основной базе данных.
Значение по умолчанию: false
Интерфейс: ServerConfig
Используется в блоке Middleware.
Указывает что EclipseLink ORM будет использовать для inner joins выражение JOIN ON вместо условий в выражении WHERE.
Значение по умолчанию: false
Используется в блоке Middleware.
cuba.useLocalServiceInvocation
При установке данного свойства в true блоки Web Client и Web Portal вызывают сервисы Middleware в обход сетевого стека, что положительно сказывается на производительности системы. Это возможно в случае быстрого развертывания в Tomcat, а также для единого WAR и единого Uber-JAR. В других вариантах развертывания данное свойство необходимо установить в false.
Значение по умолчанию: true
Используется в блоках Web Client и Web Portal.
cuba.useReadOnlyTransactionForLoad
Указывает, что все методы load в DataManager используют read-only транзакции.
Значение по умолчанию: true
Хранится в базе данных.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Задает шаблон формирования полного имени пользователя.
Значение по умолчанию: {FF| }{LL}
Полное имя можно сформировать по шаблону из имени, отчества и фамилии пользователя. В шаблоне используются следующие правила:
LL означает фамилию пользователя, написанную в полном варианте (Иванов)
L означает фамилию пользователя, написанную в кратком варианте (И)
FF означает имя пользователя, написанного в полном варианте (Петр)
F означает фамилию пользователя, написанную в кратком варианте (П)
MM означает отчество пользователя, написанное в полном варианте (Сергеевич)
M означает отчество пользователя, написанное в кратком варианте (С)
Используется в блоках Web Client и Desktop Client.
Задает шаблон отображения имени экземпляра сущности User (пользователь). Данное имя отображается, в том числе, в правом верхнем углу главного окна системы.
Значение по умолчанию: {1} [{0}]
Вместо {0} подставляется атрибут login, вместо {1} - атрибут name.
Используется в блоках Middleware, Web Client, Desktop Client.
cuba.userSessionExpirationTimeoutSec
Задает таймаут неактивности сессии пользователя в секундах.
Значение по умолчанию: 1800
Интерфейс: ServerConfig
Используется в блоке Middleware.
|
Рекомендуется выставлять параметры cuba.userSessionExpirationTimeoutSec и cuba.httpSessionExpirationTimeoutSec в одинаковое значение. |
Активирует механизм журналирования пользовательских сессий.
Значение по умолчанию: false
Хранится в базе данных.
Интерфейс: GlobalConfig.
Используется во всех стандартных блоках.
URL для соединения с блоком Middleware, через который выполняется вход пользователей в систему.
Этот параметр необходимо устанавливать в дополнительных блоках среднего слоя, которые выполняют запросы клиентов, но не содержат общего кэша пользовательских сессий. Тогда в начале выполнения запроса при отсутствии требуемой сессии в локальном кэше данный блок вызовет метод LoginService.getSession() по указанному URL, и в случае успеха закэширует полученную сессию у себя.
Интерфейс: ServerConfig
Используется в блоке Middleware.
Аддитивное свойство, задающее файл views.xml. См. Представления.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется во всех стандартных блоках.
Пример:
cuba.viewsConfig = +com/company/sample/views.xml
URL, по которому доступен Web Client приложения.
Используется, в частности, для формирования ссылок на экраны приложения извне, а также классом ScreenHistorySupport.
Значение по умолчанию: http://localhost:8080/app
Хранится в базе данных.
Интерфейс: GlobalConfig
Может использоваться во всех стандартных блоках.
Аддитивное свойство, задающее файл screens.xml.
Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.
Используется в блоках Web Client и Desktop Client.
Пример:
cuba.windowConfig = +com/company/sample/web-screens.xml
cuba.web.allowHandleBrowserHistoryBack
Позволяет обрабатывать в приложении нажатия на кнопку Back браузера путем переопределения метода AppWindow.onHistoryBackPerformed(). Если свойство установлено в true, стандартное поведение браузера заменяется на вызов этого метода.
См. Специфика Web Client.
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.appFoldersRefreshPeriodSec
Период по умолчанию обновления папок приложения в секундах.
Значение по умолчанию: 180
Интерфейс: WebConfig
Используется в блоке Web Client.
Задает начальный режим главного окна: с вкладками или одноэкранный (TABBED или SINGLE). В одноэкранном режиме экран, открываемый в режиме NEW_TAB, отображается не в новой вкладке, а полностью заменяет текущий экран.
Пользователь впоследствии может задать желаемый режим через экран Help > Settings.
Значение по умолчанию: TABBED
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.closeIdleHttpSessions
Определяет, может ли веб-клиент закрыть сессию и UI по истечению таймаута сессии после последнего non-heartbeat сообщения.
Значение по умолчанию: false
Интерфейс: WebConfig
Используется в блоке Web Client.
Аддитивное свойство, задающее файл конфигурации для компонентов приложения, поставляемых в отдельных JAR-файлах или указанных в дескрипторе cuba-ui-component.xml модуля web.
Пример:
cuba.web.componentsConfig =+demo-web-components.xml
cuba.web.defaultScreenCanBeClosed
Разрешает закрывать окно по умолчанию с помощью кнопки закрытия, контекстного меню TabSheet или нажатием клавиши ESC в случае, если выбран режим главного окна TABBED.
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
Задаёт экран, который будет открыт по умолчанию после входа в систему для всех пользователей.
Например:
cuba.web.defaultScreenId = sys$SendingMessage.browse
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.externalAuthentication
Устарело. Используйте вместо этого свойства точки расширения в Web Client.
Указывает на то, что аутентификация производится внешним механизмом, таким как LDAP или SSO Identity Provider. См. также cuba.web.externalAuthenticationProviderClass.
Значение по умолчанию: false
Интерфейс: WebAuthConfig
Используется в блоке Web Client.
cuba.web.externalAuthenticationProviderClass
Устарело. Используйте вместо этого свойства точки расширения в Web Client.
Класс, реализующий интерфейс CubaAuthProvider и используемый в случае если свойство cuba.web.externalAuthentication установлено в true.
См. разделы Интеграция с LDAP и Single-Sign-On для приложений CUBA для примеров.
Интерфейс: WebAuthConfig
Используется в блоке Web Client.
cuba.web.foldersPaneDefaultWidth
Ширина по умолчанию панели папок в пикселях.
Значение по умолчанию: 200
Интерфейс: WebConfig
Используется в блоке Web Client.
Если false, то функциональность панели папок отключена.
Значение по умолчанию: false
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.foldersPaneVisibleByDefault
Если true, то при первом входе пользователя в систему панель папок будет отображаться в развернутом состоянии, если false - то в свернутом.
Значение по умолчанию: false
Интерфейс: WebConfig
Используется в блоке Web Client.
Включить/выключить интеграцию с LDAP в Web Client.
Например:
cuba.web.ldap.enabled = true
Интерфейс: WebLdapConfig
Используется в блоке Web Client.
Указывает URL сервера LDAP.
Например:
cuba.web.ldap.urls = ldap://192.168.1.1:389
Интерфейс: WebLdapConfig
Используется в блоке Web Client.
Указывает base DN поиска имен пользователей.
Например
cuba.web.ldap.base = ou=Employees,dc=mycompany,dc=com
Интерфейс: WebLdapConfig
Используется в блоке Web Client.
Указывает distinguished name системного пользователя, имеющего право на чтение информации из LDAP.
Например:
cuba.web.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com
Интерфейс: WebLdapConfig
Используется в блоке Web Client.
Пароль системного пользователя, заданного свойством cuba.web.ldap.user.
Например:
cuba.web.ldap.password = system_user_password
Интерфейс: WebLdapConfig
Используется в блоке Web Client.
Название атрибута пользователя в LDAP, значение которого соответствует логину пользователя. По умолчанию sAMAccountName (подходит для Active Directory).
Например:
cuba.web.ldap.userLoginField = username
Интерфейс: WebLdapConfig
Используется в блоке Web Client.
Для SSO Service Provider разрешает использовать механизм логина Identity Provider.
Например:
cuba.web.idp.enabled = true
Интерфейс: WebIdpConfig
Используется в блоке Web Client.
Для SSO Service Provider включает/отключает механизм входа Identity Provider.
Например:
cuba.web.idp.enabled = true
Интерфейс: WebIdpConfig
Используется в блоке Web Client.
Для SSO Service Provider устанавливает Identity Provider URL. Стандартный CUBA IDP использует адрес idp/ (символ / в конце URL обязателен).
Например:
cuba.web.idp.baseUrl = http://main:8080/app/idp/
Интерфейс: WebIdpConfig
Используется в блоке Web Client.
cuba.web.idp.trustedServicePassword
Для SSO Service Provider устанавливает пароль, используемый в коммуникации server-to-server между SP и IDP. Должен быть равен cuba.idp.trustedServicePassword.
Интерфейс: WebIdpConfig
Используется в блоке Web Client.
Определяет список команд, передаваемых в URL, для которых вызывается обработка бином LinkHandler. См. Ссылки на экраны.
Элементы списка отделяются символом |.
Значение по умолчанию: open|o
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.loginDialogDefaultUser
Задает имя пользователя по умолчанию. Оно будет автоматически подставляться в экране входа в систему, что удобно в процессе разработки приложения. В режиме эксплуатации приложения в данном свойстве необходимо задать значение <disabled>.
Значение по умолчанию: admin
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.loginDialogDefaultPassword
Задает пароль пользователя по умолчанию. Он будет автоматически подставляться в экране входа в систему, что удобно в процессе разработки приложения. В режиме эксплуатации приложения в данном свойстве необходимо задать значение ` <disabled>`.
Значение по умолчанию: admin
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.loginDialogPoweredByLinkVisible
Установите в false, чтобы скрыть ссылку "powered by CUBA Platform" на экране входа в систему.
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
Определяет, какой компонент будет использован в режиме TABBED главного окна. Может иметь два строковых значения из перечисления MainTabSheetMode:
Значение по умолчанию: DEFAULT.
Интерфейс: WebConfig.
Используется в блоке Web Client.
cuba.web.managedMainTabSheetMode
Если свойство cuba.web.mainTabSheetMode установлено в MANAGED, определяет, как компонент главного окна переключает вкладки главного TabSheet: только скрывает их или выгружает и загружает вкладку заново при переключении.
Значение по умолчанию: HIDE_TABS
Интерфейс: WebConfig
Используется в блоке Web Client.
Задает максимальное количество вкладок с экранами, которые пользователь может открыть в главном окне приложения. Значение 0 снимает ограничение.
Значение по умолчанию: 7
Интерфейс: WebConfig
Используется в блоке Web Client.
Позволяет полностью запретить консоль разработчика Vaadin в браузере, доступную через добавление ?debug к адресу приложения, тем самым, отключает доступ к возможностям отладки JavaScript и сокращает количество информации о сервере, выдаваемой браузеру.
Значение по умолчанию: false
Интерфейс: WebConfig
Используется в блоке Web Client.
Позволяет полностью запретить server push. В этом случае механизм фоновых задач не будет работать.
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
Позволяет использовать long polling вместо WebSocket для реализации server push.
Значение по умолчанию: false
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.pushLongPollingSuspendTimeoutMs
Задает push тайм-аут в миллисекундах, который используется в случае, если включен long polling вместо WebSocket для реализации server push, т.е. cuba.web.pushLongPolling="true".
Значение по умолчанию: -1
Интерфейс: WebConfig
Используется в блоке Web Client.
Управляет отображением флажка Remember Me в стандартном экране входа в систему в веб-клиенте.
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
Задает расположение каталога, из которого могут быть загружены файлы для вывода на экран компонентом Embedded. Например:
cuba.web.resourcesRoot=${cuba.confDir}/resources
Значение по умолчанию: null
Интерфейс: WebConfig
Используется в блоке Web Client.
Позволяет скрыть панель breadcrumbs, которая раполагается в верхней части рабочей области главного окна.
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
Задает отображение значков в панели папок. Если включено, то используются следующие файлы каталога темы приложения:
Значение по умолчанию: false
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.requirePasswordForNewUsers
Если значение установлено в true, то пароль является обязательным полем при создании пользователя из Web Client. Рекомендуется устанавливаеть значение false, если в приложении используется LDAP аутентификация.
Значение по умолчанию: true
Интерфейс: WebAuthConfig
Используется в блоке Web Client.
cuba.web.standardAuthenticationUsers
Разделенный запятыми список логинов пользователей, которые могут входить в систему, используя только стандартную аутентификацию. Для этих пользователей внешняя аутентификация (например, LDAP или IDP SSO) запрещена.
Пустой список означает, что все могут использовать внешнюю аутентификацию, если она включена.
Значение по умолчанию: <empty list>
Интерфейс: WebAuthConfig
Используется в блоке Web Client.
Регулирует кэширование данных компонента Table в браузере. Количество закэшированных строк будет равняться cacheRate умноженному на pageLength как снизу так и сверху видимой области.
Значение по умолчанию: 2
Интерфейс: WebConfig
Используется в блоке Web Client.
Устанавливает количество строк, которое загружается с сервера в браузер когда компонент Table отрисовывается первый раз после обновления. См. также cuba.web.table.cacheRate.
Значение по умолчанию: 15
Интерфейс: WebConfig
Используется в блоке Web Client.
Задает имя темы, используемой по умолчанию в веб-клиенте. См. также свойство cuba.themeConfig.
Значение по умолчанию: halo
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.uiHeartbeatIntervalSec
Задаёт интервал heartbeat-сообщений для UI веб-клиента. По умолчанию будет использовано вычисляемое значение свойства cuba.httpSessionExpirationTimeoutSec / 3.
Значение по умолчанию: таймаут бездействия HTTP-сессии, сек. / 3
Интерфейс: WebConfig
Используется в блоке Web Client.
При включенном свойстве для темы halo в качестве значков стандартных действий и экранов платформы используются элементы шрифта Font Awesome вместо файлов изображений.
Соответствие между именем, указанным в свойстве icon действия или визуального компонента, и элементом шрифта, задается в файле halo-theme.properties платформы. В нем ключи, начинающиеся с cuba.web.icons соответствуют именам значков, а их значения - константам перечисления com.vaadin.server.FontAwesome. Например, элемент шрифта для значка стандартного действия create, задается строкой:
cuba.web.icons.create.png = font-icon:FILE_O
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
Для темы Halo или ее наследников управляет цветом заголовка веб-клиента. Если true, то заголовок темный (инверсный), если false - заголовок приобретает цвет основного фона приложения.
Данное свойство не действует, если в теме установлена переменная
$v-support-inverse-menu: false;
Это имеет смысл для темной темы, если пользователю дана возможность переключаться между светлой и темной темой. Тогда в светлой теме заголовок будет инверсным, а в темной основного цвета фона.
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
cuba.web.userCanChooseDefaultScreen
Определяет, может ли пользователь установить для себя окно по умолчанию. Если false, поле Default screen в экране Settings будет доступно только для чтения.
Значение по умолчанию: true
Интерфейс: WebConfig
Используется в блоке Web Client.
Задает список расширений файлов, отображаемых в окне браузера при выгрузке файла через ExportDisplay.show(). Разделителем элементов списка является символ |.
Значение по умолчанию: htm|html|jpg|png|jpeg|pdf
Интерфейс: WebConfig
Используется в блоке Web Client.
Конфигурационный параметр, задающий имя контекста веб-приложения. Как правило, эквивалентен имени каталога или WAR-файла, содержащего данный блок приложения.
Интерфейс: GlobalConfig
Используется в блоках Middleware, Web Client, Web Portal.
Например, для блока Middleware, расположенного в каталоге tomcat/webapps/app-core, и доступного по URL http://somehost:8080/app-core данное свойство должно быть задано следующим образом:
cuba.webContextName=app-core
Конфигурационный параметр, задающий имя хоста, на котором запущен данный блок приложения.
Значение по умолчанию: localhost
Интерфейс: GlobalConfig
Используется в блоках Middleware, Web Client, Web Portal.
Например, для блока Middleware, доступного по URL http://somehost:8080/app-core данное свойство должно быть задано следующим образом:
cuba.webHostName=somehost
Конфигурационный параметр, задающий имя порта, на котором запущен данный блок приложения.
Значение по умолчанию: 8080
Интерфейс: GlobalConfig
Используется в блоках* Middleware*, Web Client, Web Portal.
Например, для блока Middleware, доступного по URL http://somehost:8080/app-core данное свойство должно быть задано следующим образом:
cuba.webPort=8080
Приложение C: Системные свойства
Системные свойства задаются при запуске JVM с помощью аргумента командной строки -D и могут быть получены или установлены методами getProperty(), setProperty() класса System.
Системные свойства можно использовать для установки или переопределения значений свойств приложения. Например, следующий аргумент командной строки переопределит значение свойства cuba.connectionUrlList, которое обычно задается в файле web-app.properties:
-Dcuba.connectionUrlList=http://somehost:8080/app-core
|
Имейте в виду, что системные свойства влияют на всю JVM, то есть все блоки приложения, выполняющиеся на данной JVM, получат одинаковое значение свойства. |
|
Системные свойства кэшируются фреймворком на старте сервера, поэтому ваше приложение не должно полагаться на возможность переопределения свойства приложения с помощью изменения системного свойства во время работы приложения. Если вам абсолютно необходимо сделать это, сбросьте кэш после изменения системного свойства с помощью метода clearSystemPropertiesCache() JMX бина CachingFacadeMBean. |
Ниже приведены системные свойства, используемые в платформе, но не являющиеся свойствами приложения.
Определяет местонахождение файла конфигурации фреймворка Logback.
Для блоков приложения, работающих на веб-сервере Tomcat, данное системное свойство задается в файлах tomcat/bin/setenv.bat и tomcat/bin/setenv.sh. По умолчанию оно указывает на конфигурационный файл tomcat/conf/logback.xml.
Для Desktop Client, если данное свойство не задано при запуске JVM, оно задается в коде самого приложения и по умолчанию указывает на файл cuba-logback.xml, расположенный в корне CLASSPATH. Подробнее см. Настройка логирования в десктоп-клиенте.
Для блока Desktop Client задает расположение домашнего каталога, в котором по умолчанию находятся каталоги, определяемые свойствами приложения cuba.confDir, cuba.logDir, cuba.tempDir, cuba.dataDir.
Если данное свойство не задано при запуске JVM, то будет использовано значение ${user.home}/.haulmont/cuba, которое можно изменить в прикладном проекте, переопределив метод getDefaultHomeDir() класса com.haulmont.cuba.desktop.App.
Данное системное свойство устанавливается в значение true в режиме выполнения интеграционных тестов базовым классом CubaTestCase.
Пример использования:
if (!Boolean.valueOf(System.getProperty("cuba.unitTestMode")))
return "Not in test mode";
10. Основные определения и понятия
В контексте данного руководства под артефактом понимается файл (обычно JAR или ZIP), содержащий исполняемый или другой код, получившийся в результате сборки проекта. Артефакт имеет версию и имя, соответствующее определённым правилам, и может храниться в репозитории артефактов.
То же самое что компоненты приложения. Данный термин был принят в предыдущих версиях платформы и документации.
БД
Реляционная база данных.
Экранная форма, на которой размещается таблица со списком сущностей, а также кнопки создания, редактирования, удаления сущности.
Известно также как принцип Inversion Of Control (IoC). Механизм для получения ссылок на используемые объекты, при котором объект только декларирует, от каких объектов он зависит, а контейнер создает нужные объекты и инжектирует в зависимый объект.
См. https://ru.wikipedia.org/wiki/Внедрение_зависимости.
Главный пакет сообщений
Загрузка данных подклассов и связанных объектов одновременно с основной запрашиваемой сущностью.
Источник данных
См. Источники данных.
Контейнер управляет жизненным циклом и конфигурацией программных объектов. Является базовым компонентом технологии Dependency Injection (или Inversion of Control).
В платформе CUBA используется контейнер Spring Framework.
Java класс, содержащий логику инициализации и обработки событий экрана. Связан с XML-дескриптором экрана.
См. Контроллер экрана.
Атрибут сущности, не являющийся ссылкой или коллекцией ссылок на другую сущность. Значения всех локальных атрибутов сущности, как правило, хранятся в одной таблице (исключение составляют некоторые стратегии наследования сущностей).
Пакет локализованных сообщений
См. Пакеты сообщений.
Набор экземпляров сущностей, загруженных из базы данных или только что созданных. Персистентный контекст является кэшем данных в рамках текущей транзакции. При коммите транзакции все изменения сущностей в персистентном контексте сохраняются в БД.
См. Entity Manager.
Представление
См. Представления.
Сервер, осуществляющий хранение артефактов в определенной структуре. В процессе сборки некоторого проекта из репозитория загружаются артефакты, от которых зависит данный проект.
Основной элемент модели данных, см. Модель данных.
Application Tiers
См. Уровни и блоки приложения.
Свойства приложения − именованные данные различных типов, определяющие всевозможные аспекты конфигурации и функционирования приложения.
Application Units
См. Уровни и блоки приложения.
Datasource
См. Источники данных.
Eager Fetching
См. Жадная загрузка.
EntityManager
Программный компонент среднего слоя, служащий для работы с персистентными сущностями.
См. EntityManager
Groovy — объектно-ориентированный язык программирования, разработанный для платформы Java как дополнение к языку Java с возможностями Python, Ruby и Smalltalk.
Элемент AOP (Aspect Oriented Programming), позволяющий изменить или расширить обычный вызов метода объекта.
См. https://en.wikipedia.org/wiki/Interceptor_pattern.
Упрощенный профиль Java Enterprise Edition, разработанный для веб-приложений, для которых не требуются такие технологии как EJB, JTA и т.д.
Java Management Extensions − технология, которая предоставляет инструменты для управления приложениями, объектами системы, устройствами. Определяет стандарт для написания JMX-компонентов − MBeans.
Более подробную информацию можно найти по адресу: http://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html
См. также Использование инструментов JMX.
Java Persistence API - стандартная спецификация технологии объектно-реляционного отображения (ORM). В платформе CUBA используется фреймворк EclipseLink, реализующий эту спецификацию.
Платформо-независимый объектно-ориентированный язык запросов, определенный как часть спецификации JPA.
Более подробную информацию можно найти по адресу https://en.wikibooks.org/wiki/Java_Persistence/JPQL.
Lazy loading
Managed Beans
Программные компоненты Middleware, содержащие бизнес-логику приложения.
См. Управляемые бины.
MBeans
Managed Beans, имеющие JMX-интерфейс. Как правило, имеют внутреннее состояние (например, кэш, конфигурационные данные или статистику), к которому нужно обеспечить доступ через JMX.
Средний слой − уровень приложения, содержащий бизнес-логику, работающий с базой данных, и предоставляющий общий интерфейс для верхних (клиентских) уровней приложения.
Оптимистичная блокировка - способ управления совместным доступом к данным различными пользователями, при котором предполагается, что возможность одновременного изменения ими одного и того же экземпляра сущности мала. В этом случае блокировка как таковая отсутствует, вместо нее в момент сохранения изменений производится проверка, нет ли в БД более новой версии данных, сохраненной другим пользователем. Если есть, выбрасывается исключение, и текущий пользователь должен снова загрузить данный экземпляр сущности.
См. также https://en.wikipedia.org/wiki/Optimistic_concurrency_control
ORM
Object-Relational Mapping - объектно-реляционное отображение - технология связывания таблиц реляционной базы данных с объектами языка программирования.
См. Слой ORM.
Services
Сервисы среднего слоя предоставляют интерфейс для вызова бизнес-логики клиентами и образуют границу Middleware. Сервисы могут содержать бизнес-логику внутри себя, либо делегировать выполнение Managed Beans.
См. Сервисы.
Single Sign-On, SSO
Технология, при использовании которой пользователь переходит от одного приложения к другому без повторной аутентификации. Интеграция CUBA-приложения с Active Directory позволяет пользователям Windows входить в приложение без ввода имени и пароля.
Soft deletion
См. Мягкое удаление.
UI
User Interface - пользовательский интерфейс.
View
См. Представления.
Файл в формате XML, содержащий описание источников данных и расположения визуальных компонентов экрана.
См. XML-дескриптор.